简介:基于Android Studio集成科大讯飞AIKit语音唤醒的纯净版安卓工程资源包,面向安卓开发者和需要快速落地语音唤醒功能的学习者,提供从SDK接入、依赖配置到唤醒词设置与麦克风权限处理的完整实现代码。压缩包共647个文件,约45.56MB,包含Java源码、Gradle构建脚本、XML界面与清单配置、JAR/SO原生库及可安装APK,基本覆盖工程编译、资源定义和底层语音识别所需内容。目前已有719人下载学习。工程内含有语音唤醒初始化、唤醒词属性配置、误唤醒调试等关键模块,可直接导入Android Studio参考或改造;同时需留意部分文件采用加密或混淆命名,建议保持原目录结构导入,并在真机上验证不同距离、音量下的唤醒效果。
1. 从一条“能编译但喊不醒”的日志说起:这份AI Kit语音唤醒Demo到底做了什么
拿到这份源码的第一个晚上,我按惯性思维给Android Studio项目同步了Gradle、接了SDK,真机一跑,应用没崩,代码也编译过了,结果对麦克风连喊五遍唤醒词,日志里连个回调都没有。后来排查下来,问题不在核心代码,而是SDK的初始化位置、so库架构和回调注册时序这三个最容易忽略的前置细节。这个项目就是一套基于Android Studio接入科大讯飞AI Kit语音唤醒的纯净版Demo:它只做一件事——说唤醒词,引擎离线命中,回调触发App状态更新。适合两类人:一类是想给自己的App加一个语音入口的安卓开发,另一类是官方文档翻了几页没跑通、想找一份对照工程来调试集成细节的开发者。核心价值是把唤醒链路做到最小可跑,方便你在此基础上换唤醒词、接后续逻辑。
2. 技术选型与SDK架构:三条唤醒路线里为什么选AI Kit
2.1 路线对比:IVW、AI Kit与LiveQuery怎么选
科大讯飞语音唤醒能力现在公开的接入路线大致有三条:老牌的IVW离线唤醒SDK、新一代的AI Kit统一SDK、以及需要联网的LiveQuery云端交互方案。拆这份源码之前,我先把这三条路线的差异理了一遍,避免在Demo里用错接口方向。
IVW是最早一批安卓唤醒SDK,功能纯粹,只做本地唤醒词检测,代码风格偏传统,需要在官网单独下载离线资源包,再手动把jar和so搬进工程。AI Kit定位是新一代的统一能力入口,把唤醒、识别、合成等多个服务收口到一个SDK框架里管理,唤醒只是其中一个服务模块,接口风格更统一,包体也不会把用不到的能力塞进来——这正是这份源码叫“纯净版”的含义:只集成唤醒模块,不打包用不上的识别和合成库。LiveQuery走的是云端交互,依赖网络,适合做带语义理解的对话型应用,本地离线唤醒场景用不上。
三条路线的选择判断其实很简单:如果你的场景是“息屏状态下喊一声唤醒App”,必须吃本地离线唤醒,那就放弃LiveQuery;如果是从零开始的新项目,优先跟AI Kit走,因为接口维护和版本升级都更省心;如果是祖传老工程已经在用IVW且跑得稳定,倒也没必要为了换而换。这份源码选AI Kit,我看中的是它的API收敛做得更彻底——同一个管理器负责初始化和释放,回调链路比两套接口互相调的老写法清晰不少。
| 对比项 | IVW离线唤醒 | AI Kit语音唤醒 | LiveQuery |
|---|---|---|---|
| 是否依赖网络 | 否,纯本地 | 首启可选联网校验,唤醒本地执行 | 是,全程联网 |
| 集成复杂度 | 低,接口较老 | 中,统一管理器收口 | 中,需处理网络异常 |
| 包体影响 | 只带唤醒相关库 | 按需引入服务模块 | 依赖更多通信库 |
| 适合场景 | 老项目维护 | 新项目、纯净唤醒需求 | 需要云端语义交互 |
2.2 唤醒生命周期:声音从麦克风到UI回调要经过什么
跑通这个Demo之前,先理解AI Kit唤醒的一次完整生命周期,后面看代码会顺很多。我把这条链路拆成五个环节:权限触发、音频采集、特征比对、回调分发、UI刷新。
应用启动后,先完成SDK初始化并注册回调监听,此时引擎处于待机状态。用户说出唤醒词后,麦克风通过AudioRecord采集音频流,本地唤醒引擎对音频做特征提取,和预先录入的唤醒词模型比对。命中后引擎抛出一个结果对象,SDK内部把它封装成回调事件分发到主线程,最终落到你在代码里实现的Listener接口上。一次完整的唤醒周期就是从“IDLE待机”到“LISTENING收声中”,再回到“IDLE准备下一次”的状态循环。
这里我一般会重点盯着两个地方:一是确保注册回调在启动监听之前完成,否则引擎启动后第一个结果会没人接;二是状态标记位要处理好,避免在唤醒回调里立刻又调用启动监听,造成叠听。常见做法是在每次回调结束后主动将状态复位,再决定要不要继续监听。这个细节在你做“唤醒一次就跳页”的场景里特别明显,很多人的App唤醒后第二次喊就没反应,多半就是状态卡在WAKEUP没回来。
2.3 工程前置配置:清单文件、so库与混淆规则
代码写得再对,工程配置缺一环都跑不起来。先说AndroidManifest.xml,核心是三件事:申请录音权限、配置AppID的meta-data、注册唤醒服务或Activity。录音权限用RECORD_AUDIO,并且Android 6.0以后必须代码动态申请,光在清单里写是不够的。AppID通过com.iflytek.cloud.appid这个meta-data传进去,也可以在Application里用代码初始化,两份源码走的是代码初始化,但清单里的meta-data依然保留,因为部分SDK组件启动时会读取。
<uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.INTERNET" /> <application android:name=".SpeechApp" android:label="@string/app_name"> <meta-data android:name="com.iflytek.cloud.appid" android:value="你的AppID" /> </application>这段清单里INTERNET权限很多人会漏掉。虽然唤醒引擎本身是离线识别,但SDK首次激活或联网校验AppID时会走一次网络请求,如果清单里没有INTERNET权限,初始化接口会静默失败,返回true的概率很低。我拆这份源码时把INTERNET权限去掉跑过一次,现象是初始化好像成功了,但唤醒一直不触发,日志里只有一条不起眼的网络异常。
so库的放置位置也容易翻车。AI Kit的so文件要放在app/src/main/jniLibs/目录下,并且建议同时保留armeabi-v7a和arm64-v8a两个架构。真机大多是64位,如果只放v7a,部分机型可以跑,部分机型会在加载so时报UnsatisfiedLinkError。如果你用的是Android Studio 4.0以后的版本,还可以在build.gradle里显式指定jniLibs的路径,把so放到libs目录统一管理。
混淆规则方面,SDK自带jar包内部有大量反射和JNI调用,混淆后类名一变就找不到native方法。我一般会在proguard-rules.pro里直接加一整段keep:
-keep class com.iflytek.**{*;} -keep class com.sunrise.**{*;} -keepclassmembers class * { native <methods>; }这里com.iflytek是SDK主包,控件和引擎类都集中在里面;com.sunrise是语音引擎底层实现包,不keep的话容易出现“初始化成功但解析音频时崩溃”的诡异问题。keepclassmembers配合native是为了保住JNI方法映射。这三条规则是跑通Demo的保底写法,建议直接抄进你的工程。
3. 工程结构与运行准备:从Gradle配置到真机跑通
3.1 目录结构:这份源码每个目录是干什么的
把源码在Android Studio里打开后,建议先花两分钟过一遍目录,别急着点Run。工程结构是标准的单模块App,加上SDK资源,目录树如下:
SpeechWakeupDemo/ ├── app/ │ ├── libs/ │ │ └── SunflowerService.jar # AI Kit核心jar包 │ ├── src/ │ │ └── main/ │ │ ├── assets/ # 唤醒词资源与SDK配置 │ │ │ ├── wakeup_resource.jet # 离线唤醒词模型文件 │ │ │ └── msc.cfg # 引擎运行配置文件 │ │ ├── java/com/iflytek/demo/ │ │ │ ├── SpeechApp.java # 应用入口,SDK初始化 │ │ │ ├── WakeUpActivity.java # 主界面,状态展示与交互 │ │ │ └── WakeUpService.java # 后台唤醒监听服务 │ │ ├── jniLibs/ │ │ │ ├── arm64-v8a/ │ │ │ │ ├── libmsc.so # 64位引擎动态库 │ │ │ │ └── libivw.so # 唤醒专用动态库 │ │ │ └── armeabi-v7a/ │ │ │ ├── libmsc.so │ │ │ └── libivw.so │ │ └── res/ │ └── build.gradle ├── gradle/ ├── build.gradle └── settings.gradlelibs目录下的SunflowerService.jar是AI Kit的统一能力包,唤醒、识别、合成都在这个jar里,但只有你调用唤醒模块时才会加载对应native逻辑,这是“纯净”的体现。assets下的wakeup_resource.jet是唤醒词离线模型,默认内置了“你好小飞”这类通用唤醒词,后面二次开发要做的就是替换这个文件。msc.cfg是引擎参数配置,一般不用动。
3.2 app模块build.gradle:依赖、libs与jniLibs的写法
这个工程的依赖配置有讲究。AI Kit的jar包不走远程仓库,而是放在本地libs目录,所以build.gradle里需要手动指定jar依赖和so路径。我看过很多新手直接在这步卡住——jar复制进来了,但没加implementation fileTree,编译一路报“找不到类”。
android { compileSdk 34 defaultConfig { applicationId "com.iflytek.demo" minSdk 21 targetSdk 34 ndk { abiFilters 'armeabi-v7a', 'arm64-v8a' } } sourceSets { main { jniLibs.srcDirs = ['src/main/jniLibs'] } } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'androidx.appcompat:appcompat:1.6.1' }这里的fileTree写法会把libs目录下所有jar都引入,后续你如果想加AI Kit的识别功能,同样往这个目录丢jar就行。ndk.abiFilters限定了只编两个架构的so,避免打包时把多余的x86也打进去,减小APK体积。jniLibs.srcDirs是显式声明so目录,Android Studio默认会找src/main/jniLibs,这里写出来是给后续想挪so目录的人留个改入口。
3.3 跑通四步:第一声唤醒词怎么出
整个Demo跑通只需要四步,每一步都可能踩坑,我按顺序说。
第一步,用Android Studio打开工程根目录,等Gradle同步完成。如果同步报错,先检查Gradle JDK版本和Android SDK Build Tools是否本机已安装,这是Android Studio项目的通用问题,跟讯飞SDK无关。第二步,在SpeechApp.java里填入你自己的AppID。这个AppID要提前在讯飞开放平台创建应用后获取,注意包名必须和创建应用时填写的包名一致,否则即使初始化返回成功,唤醒也不会生效——SDK会在内部核对包名和AppID的绑定关系。第三步,真机连接电脑,开启USB调试,直接Run到设备上。建议用真机而不是模拟器,模拟器的麦克风输入和音频采样链路跟真机差异很大,唤醒引擎在模拟器上基本不工作。第四步,点击主界面的“开始唤醒”按钮,授权录音权限,等界面状态从“已停止”切到“聆听中”,这时候对着手机说出内置唤醒词。
这四步走完,如果一切正常,你会看到界面状态跳转到“已唤醒”,同时日志里打出唤醒结果数据。没出声音的话,优先回头看第2.3节里的manifest配置和so目录,九成问题都在那里。
4. 核心代码拆解:从唤醒词出声到UI亮起的完整链路
4.1 初始化入口:Application里要做的事
AI Kit的SDK初始化放在SpeechApp.java里,而不是Activity的onCreate。这么做的原因是:语音引擎是全局单例,如果跟着Activity生命周期走,页面销毁重建时引擎会被反复创建和释放,轻则回调丢失,重则native层崩溃。源码用Application持有引擎实例,保证整个进程生命周期内只有一份。
public class SpeechApp extends Application { @Override public void onCreate() { super.onCreate(); // 使用application context初始化AI Kit统一入口 SpeechUtility.createUtility(this, SpeechConstant.APPID + "=你的AppID"); // 创建唤醒处理器,全局单例,后续Activity和Service共享同一个实例 aikitWakeuper = aikitWakeuper.createWakeuper(this); } public aikitWakeuper getWakeuper() { return aikitWakeuper; } }两个关键点。第一,createUtility的context参数必须是getApplicationContext(),如果传Activity实例,初始化成功后Activity退出会导致SDK底层持有的context失效,后面回调时偶发空指针。第二,createWakeuper返回的是全局唯一的唤醒引擎对象,不要在每个页面里重复调用create再destroy,正确的做法是Application里建好,页面通过get()方法取出来用,引擎的生命周期交给进程。
4.2 唤醒引擎与回调:监听、命中、超时
核心逻辑集中在WakeUpActivity.java里。启动唤醒的代码看似简单,但顺序错了就没有回调。我拆这段源码时注意到一个细节:源码里的注册回调动作放在了startListening之前,并且回调接口里对结果类型做了区分处理。
private void startWakeUp() { // 1. 注册唤醒回调,必须在启动监听之前 aikitWakeuper.setParameter(aikitWakeuper.WAKEUP_RES_PATH, "assets:///wakeup_resource.jet"); aikitWakeuper.setParameter(aikitWakeuper.WAKEUP_THRESHOLD, "3"); aikitWakeuper.setParameter(aikitWakeuper.WAKEUP_UI, "false"); aikitWakeuper.registerListener(new WakeuperListener() { @Override public void onResult(WakeuperResult result) { String json = result.getResultString(); // 结果为1表示命中唤醒词 if ("1".equals(json)) { runOnUiThread(() -> updateState("已唤醒")); } } @Override public void onError(int errorCode) { runOnUiThread(() -> updateState("监听异常:" + errorCode)); } }); // 2. 启动监听,传入本次唤醒的会话参数 aikitWakeuper.startListening(new WakeuperSession()); }这里WAKEUP_RES_PATH指定的是离线唤醒词资源路径,assets:///前缀表示从assets目录读取。WAKEUP_THRESHOLD是唤醒灵敏度,取值范围一般是1到5,数值越大越灵敏但误报也越多,3是平衡点。WAKEUP_UI设为false表示不启用SDK自带的唤醒界面,由我们自己画UI,这正是“纯净版”的意思。onResult回调里拿到的json字符串是结果状态码,有些场景还需要从WakeuperResult里取音频和得分,但那属于高级用法,这份Demo的落地场景只需要状态变化。
值得反复强调的是,onResult回调里对UI的更新一定用runOnUiThread包一层,因为唤醒引擎的回调线程在native层,不保证是主线程。直接更新TextView会偶尔遇到CalledFromWrongThreadException,这种崩溃时好时坏,极难复现,我一般在集成阶段就统一包一层。
4.3 状态刷新与资源释放:Activity收尾细节
Demo的Activity侧维护了一个三态状态机:STOPPED(已停止)、LISTENING(聆听中)、WAKEUP(已唤醒)。每次状态变化都会翻新按钮文案和状态TextView,这个交互逻辑不复杂,但释放资源的顺序值得反复看。源码里onDestroy的写法是我想强调的:
@Override protected void onDestroy() { super.onDestroy(); // 释放顺序:先停监听,再注销回调,最后销毁引擎 aikitWakeuper.stopListening(); aikitWakeuper.unregisterListener(); SpeechApp.getApp().getWakeuper().destroy(); }三点注意事项。第一,destroy()是全局引擎销毁,它会把SDK持有的音频设备释放掉并停止native层线程池,所以调用完之后整个应用内再想唤醒只能重新初始化,代价很大。第二,如果你在Application里创建引擎只是为了某个页面服务,那么Activity销毁时不应该调destroy,只要stopListening和unregisterListener就够了,这样可以保证下次进入页面还能快速再启动。第三,很多人会在onPause里直接调destroy来防止后台耗电,我理解这个意图,但这会导致每次回到页面都重新加载唤醒词模型,出现“第一次唤醒总是慢半拍”的体验。更好的做法是onPause只暂停监听,保留引擎实例。
5. 避坑实录:讯飞AI Kit唤醒集成最常见的5个问题
5.1 坑一:初始化接口返回false,但没人看日志
现象:SpeechUtility.createUtility之后,代码里调用isAvailable或getWakeuper拿到空对象,启动监听直接闪退。原因:初始化失败往往不抛异常,而是打一条WARN级别日志说明失败原因,大多数人没看logcat的习惯。解决:初始化后立刻打印返回值和错误日志,定位是AppID非法、包名不匹配还是缺少so。最常见的是AppID没填就初始化,SDK返回错误码21001(无效参数),从开放平台复制AppID时注意别带空格。
5.2 坑二:so库架构缺失导致UnsatisfiedLinkError
现象:App在部分设备上正常,在另一台设备上启动即崩,crash日志指向libivw.so。原因:工程只放了armeabi-v7a或只放了arm64-v8a,而设备的ABI恰好是缺失的那个;通过abiFilters把架构过滤掉也能触发。解决:jniLibs下同时保留两个主流ABI目录,并且在abiFilters里显式声明。血泪经验是别为了“减包体”只留一个架构——你的用户设备分布永远比你想象的杂。
5.3 坑三:注册回调前就启动了监听,唤醒结果永远没人接
现象:代码逻辑看着没问题,程序也不报错,但就是没有任何日志输出。原因:registerListener和startListening的调用顺序反了,引擎启动后发现无监听者,直接丢弃了唤醒结果。解决:把注册回调放在startListening之前,并且检查你的回调对象是否有被编译器优化掉的风险,比如直接new在方法内部而没有任何强引用持有。这里有个玄学细节:如果Listener只作为临时对象传递,某些情况下会被GC回收,导致后来回调到空引用。我一般在类成员变量里持有一份Listener引用。
5.4 坑四:Android 12以上后台启动Service受限,息屏唤醒失效
现象:亮屏时唤醒正常,锁屏放口袋再喊,什么反应都没有。原因:从Android 12(targetSdk 31+)开始,后台应用启动前台Service有限制,而唤醒监听要是放在普通Service里,进程进入后台后音频采集就会被系统冻结。解决:把WakeUpService改成前台服务,在onCreate里调用startForeground并传入一条常驻通知,同时保证录音权限在后台仍然有效。如果你要在锁屏场景做唤醒,这是绕不过去的一步。
5.5 坑五:混淆后回调触发但结果全空
现象:发布release包后,能监听到有回调,但onResult里拿到的json是一串空值。原因:SDK内部用反射解析结果对象,混淆把解析用的字段名改了。解决:按第2.3节里的proguard规则完整keep讯飞SDK的包名,并且确认native方法keep开启。这类问题在debug包上不会出现,只有打包发布时才踩中,极其容易忽略。
6. 二次开发指南:把“OK 讯飞”替换成你自己的品牌词
这份Demo最值钱的地方在于,你不需要动Java代码就能换掉唤醒词。语音唤醒的核心资源是assets/wakeup_resource.jet这个模型文件,它由云端训练生成,和AppID、包名是一一绑定的。你要去开放平台的语音唤醒服务里上传自定义唤醒词,比如“小光同学”,按平台引导录入录音样本,训练完成后再下载对应资源包。拿到新资源文件后,替换掉assets目录下的旧文件,并把SpeechApp里的AppID换成你创建唤醒词时绑定的那个,代码一行都不用改。
如果你想在唤醒命中后做点实际的事,比如跳转到搜索页或拉起自定义对话框,只需要在onResult回调里加一段Intent跳转。注意跳转前先调用stopListening停掉监听,避免页面跳转过程中麦克风还在收声,把当前页面的操作音也当成唤醒词录进去。顺带一提,如果你希望唤醒灵敏度更贴合使用环境,可以把WAKEUP_THRESHOLD参数做成一个设置项,让用户在“灵敏”和“稳定”之间自己调,实测比写死在代码里效果好得多。
改动完记得做一套验证清单:冷启动后首次唤醒能否命中、连续唤醒三次后是否还能稳定触发、切入后台再返回是否恢复监听、release包在混淆状态下是否正常。四步全过,再考虑发布。从那以后我每次接语音类SDK,都强制走一遍“权限、初始化、回调顺序、释放、混淆”这五步清单,还真没再翻过车。希望帮到你。
本文还有配套的精品资源,点击获取