最近需要接入sdk,现把接入科大讯飞SDK安卓端的过程整理如下:
接入SDK分成两个大的部分:安卓端 和 Unity端
安卓端主要负责导入科大讯飞的sdk,并编写相关脚本,最后生成aar包,提供给Unity使用
Unity端则根据导出的aar包调用相关方法实现科大语音的功能
本文使用的工具是:Android Studio, Unity2018.4.1f1
一、安卓端:
1.打开Android Studio,新建项目:
?设置项目名称和sdk:
PS:本项目中使用的Android SDK可以将之前eclipse项目中用到的sdk拷贝一份,作为Android Studio使用的sdk。也可以直接下载文末附上的SDK链接中的内容
原因:Android Studio中的sdk路径最好跟eclipse使用的区分开,因为sdk更新之后有可能会导致eclipse项目无法使用 —— 根据项目具体情况有概率出现
创建完成后界面如下:
切换视图到“Project”页签:
切换后界面如下:
2.新建Module:
选中该项目,右键“New -》Module”:
设置Module name:
创建完成后界面如下:
?以下安卓端所有的操作都在此“voiceLib”文件夹下完成。
3.导入需要的jar包:
***先导入Unity的Classes.jar包:—— 这里使用的Unity2018.4.1f1,选择Mono打包方式
D:\Work\ProgramFiles\Unity\Editor\Data\PlaybackEngines\AndroidPlayer\Variations\mono\Release\Classes\Classes.jar
选中该“Classes.jar”,Ctrl + C复制一份
在AndroidStudio中选中“voiceLib ->libs”,鼠标右键“Paste”:
***导入科大讯飞的jar包:
在科大讯飞官网注册一个产品:控制台-讯飞开放平台
注册完成即能得到如下页面:
如何下载科大讯飞SDK:
点击页面上方的“平台首页”:
?点击菜单栏:“SDK下载”
?选择对应的应用,平台以及需要接入的语音功能后下载SDK:
下载完成后解压:
?选择“libs”文件夹,将该文件夹下的三个文件都拷贝到AndroidStudio项目的“voiceLib ->libs”下:
?以上一共导入4个文件到AndroidStudio项目中:
?*** 关联两个jar包:
选中以上两个jar包,鼠标右键选择“Add as library”:
注意:没有关联之前,两个jar包是无法展开的;关联之后则可以
查看关联结果:选中该module,鼠标右键选择“Open Module Settings”
如果以上可以看到这两个jar包则代表关联成功
4.导入“libmsc.so”文件:
在“src-》main”下新建“Jnilibs”文件夹:
注意文件夹名字不能写错:
将科大讯飞SDK中“libs”文件夹下的“armeabi-v7a"复制进来:
效果如下:
如此需要导入到AndroidStudio项目中的文件都导入完成了
5.修改Mainfest.xml文件:
将“app ->src ->main”文件夹下的“AndroidMainfest.xml"的内容复制一份:
?
?拷贝到刚才新建的Module:“voiceLib ->src -> main”下的“AndroidMainfest.xml”中:
粘贴之后效果如下:
粘贴之后会报红色的错误,所以还需要修改该文件。将该文件修改成以下效果即可:
注意:
1."voiceManager“:这里需要改成继承了“UnityPlayerActivity"的类,作为入口存在 —— 这个类后面会创建,并编写代码
2.这行代码很重要,一定要有,否则Unity会调用不到
<meta-data android:name="unityplayer.UnityActivity" android:value="true" />
之后在该”AndroidMainfest.xml”中插入以下权限描述:
<!--连接网络权限,用于执行云端语音能力 -->
<uses-permission android:name="android.permission.INTERNET"/>
<!--获取手机录音机使用权限,听写、识别、语义理解需要用到此权限 -->
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<!--读取网络信息状态 -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<!--获取当前wifi状态 -->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<!--允许程序改变网络连接状态 -->
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<!--读取手机信息权限 -->
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<!--读取联系人权限,上传联系人需要用到此权限 -->
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<!--外存储写权限,构建语法需要用到此权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!--外存储读权限,构建语法需要用到此权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<!--配置权限,用来记录应用配置信息 -->
<uses-permission android:name="android.permission.WRITE_SETTINGS"
tools:ignore="ProtectedPermissions" />
<!--手机定位信息,用来为语义等功能提供定位,提供更精准的服务-->
<!--定位信息是敏感信息,可通过Setting.setLocationEnable(false)关闭定位请求 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<!--如需使用人脸识别,还要添加:摄像头权限,拍照需要用到 -->
<uses-permission android:name="android.permission.CAMERA" />
?粘贴之后可能有报错:
、
点击以上选项修复报错即可。
如此则修改完成。“voiceLib -> src -> main”下的“AndroidMainfest.xml”文件完整代码如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.frank.voicelib">
<application
android:allowBackup="true"
android:label="voicelib"
android:supportsRtl="true">
<activity android:name=".voiceManager">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data android:name="unityplayer.UnityActivity" android:value="true"/>
</activity>
</application>
<!--连接网络权限,用于执行云端语音能力 -->
<uses-permission android:name="android.permission.INTERNET"/>
<!--获取手机录音机使用权限,听写、识别、语义理解需要用到此权限 -->
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<!--读取网络信息状态 -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<!--获取当前wifi状态 -->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<!--允许程序改变网络连接状态 -->
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<!--读取手机信息权限 -->
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<!--读取联系人权限,上传联系人需要用到此权限 -->
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<!--外存储写权限,构建语法需要用到此权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!--外存储读权限,构建语法需要用到此权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<!--配置权限,用来记录应用配置信息 -->
<uses-permission android:name="android.permission.WRITE_SETTINGS"
tools:ignore="ProtectedPermissions" />
<!--手机定位信息,用来为语义等功能提供定位,提供更精准的服务-->
<!--定位信息是敏感信息,可通过Setting.setLocationEnable(false)关闭定位请求 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<!--如需使用人脸识别,还要添加:摄像头权限,拍照需要用到 -->
<uses-permission android:name="android.permission.CAMERA" />
</manifest>
如此项目环境搭建完成。
6.编写Android端代码:
在以下路径创建两个java文件:
“JsonParser.java”脚本如下:
package com.frank.voicelib;
import org.json.JSONArray;
import org.json.JSONObject;
import org.json.JSONTokener;
/**
* Json结果解析类
*/
public class JsonParser {
public static String parseIatResult(String json) {
StringBuffer ret = new StringBuffer();
try {
JSONTokener tokener = new JSONTokener(json);
JSONObject joResult = new JSONObject(tokener);
JSONArray words = joResult.getJSONArray("ws");
for (int i = 0; i < words.length(); i++) {
// 转写结果词,默认使用第一个结果
JSONArray items = words.getJSONObject(i).getJSONArray("cw");
JSONObject obj = items.getJSONObject(0);
ret.append(obj.getString("w"));
// 如果需要多候选结果,解析数组其他字段
// for(int j = 0; j < items.length(); j++)
// {
// JSONObject obj = items.getJSONObject(j);
// ret.append(obj.getString("w"));
// }
}
} catch (Exception e) {
e.printStackTrace();
}
return ret.toString();
}
public static String parseGrammarResult(String json) {
StringBuffer ret = new StringBuffer();
try {
JSONTokener tokener = new JSONTokener(json);
JSONObject joResult = new JSONObject(tokener);
JSONArray words = joResult.getJSONArray("ws");
for (int i = 0; i < words.length(); i++) {
JSONArray items = words.getJSONObject(i).getJSONArray("cw");
for(int j = 0; j < items.length(); j++)
{
JSONObject obj = items.getJSONObject(j);
if(obj.getString("w").contains("nomatch"))
{
ret.append("没有匹配结果.");
return ret.toString();
}
ret.append("【结果】" + obj.getString("w"));
ret.append("【置信度】" + obj.getInt("sc"));
ret.append("\n");
}
}
} catch (Exception e) {
e.printStackTrace();
ret.append("没有匹配结果.");
}
return ret.toString();
}
public static String parseLocalGrammarResult(String json) {
StringBuffer ret = new StringBuffer();
try {
JSONTokener tokener = new JSONTokener(json);
JSONObject joResult = new JSONObject(tokener);
JSONArray words = joResult.getJSONArray("ws");
for (int i = 0; i < words.length(); i++) {
JSONArray items = words.getJSONObject(i).getJSONArray("cw");
for(int j = 0; j < items.length(); j++)
{
JSONObject obj = items.getJSONObject(j);
if(obj.getString("w").contains("nomatch"))
{
ret.append("没有匹配结果.");
return ret.toString();
}
ret.append("【结果】" + obj.getString("w"));
ret.append("\n");
}
}
ret.append("【置信度】" + joResult.optInt("sc"));
} catch (Exception e) {
e.printStackTrace();
ret.append("没有匹配结果.");
}
return ret.toString();
}
public static String parseTransResult(String json,String key) {
StringBuffer ret = new StringBuffer();
try {
JSONTokener tokener = new JSONTokener(json);
JSONObject joResult = new JSONObject(tokener);
String errorCode = joResult.optString("ret");
if(!errorCode.equals("0")) {
return joResult.optString("errmsg");
}
JSONObject transResult = joResult.optJSONObject("trans_result");
ret.append(transResult.optString(key));
/*JSONArray words = joResult.getJSONArray("results");
for (int i = 0; i < words.length(); i++) {
JSONObject obj = words.getJSONObject(i);
ret.append(obj.getString(key));
}*/
} catch (Exception e) {
e.printStackTrace();
}
return ret.toString();
}
}
“voiceManager.java"脚本如下:
package com.frank.voicelib;
import android.Manifest;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.util.Log;
import com.iflytek.cloud.RecognizerListener;
import com.iflytek.cloud.RecognizerResult;
import com.iflytek.cloud.SpeechConstant;
import com.iflytek.cloud.SpeechError;
import com.iflytek.cloud.SpeechRecognizer;
import com.iflytek.cloud.SpeechUtility;
import com.unity3d.player.UnityPlayer;
import com.unity3d.player.UnityPlayerActivity;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.HashMap;
import java.util.LinkedHashMap;
public class voiceManager extends UnityPlayerActivity{
private SpeechRecognizer mIat; //语音识别器
private RecognizerListener mRecognizerLis; //语音识别监听接口
private HashMap<String, String> mIatResults = new LinkedHashMap<String, String>(); //语音识别结果
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getPermission(); //获取权限
this.InitializeSpeechRecognizer(); //初始化语音识别
}
//开始语音识别,提供给Unity调用
public void beginListen(){
Log.d("@@@", "开始判断权限是否获取 ");
//使用录音前首先需要获取权限
int permissionCheck = this.checkSelfPermission(android.Manifest.permission.RECORD_AUDIO);
//如果有权限则返回PackageManager.PERMISSION_GRANTED,否则返回PackageManager.PERMISSION_DENIED。
if(permissionCheck!= PackageManager.PERMISSION_GRANTED) {//未获取权限时
//请求获取权限
Log.d("@@@", "正在请求权限 ");
this.requestPermissions(new String[]{Manifest.permission.RECORD_AUDIO}, 0x01);
//用new String[]的原因是可以在String[]中存储多个需要的权限,一次过请求
//将回调onRequestPermissionsResult()方法
}else {
//开始识别
mIat.startListening(mRecognizerLis);
}
}
//region 由于语音识别功能一定要获取设备的“RECORD_AUDIO"权限,否则该功能无法正常运行
//获取权限
public void getPermission() {
Log.d("@@@", "开始获取各类权限 ");
this.requestPermissions(new String[]{Manifest.permission.RECORD_AUDIO}, 0x01);
Log.d("@@@", "各类权限获取成功 ");
}
//权限获取的回调结果
public void onRequestPermissionsResult(int requestCode,
String permissions[], int[] grantResults) {
switch (requestCode) {
case 0x01: {
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Log.d("@@@", "进入获取权限成功的回调了 ");
// permission was granted, yay! Do the
// contacts-related task you need to do.
UnityPlayer.UnitySendMessage("VoiceController", "GetPermissionMsgFromAndroid", "获取到录音的权限");
} else if(grantResults[0] != PackageManager.PERMISSION_GRANTED)
{
// permission denied, boo! Disable the
// functionality that depends on this permission.
Log.d("@@@", "进入获取权限失败的回调了 ");
UnityPlayer.UnitySendMessage("VoiceController", "GetPermissionMsgFromAndroid", "没有获取到权限");
}
return;
}
}
}
//endregion
//region 语音识别功能初始化,监听接口以及识别结果输出
//初始化语音识别功能
public void InitializeSpeechRecognizer(){
//初始化
SpeechUtility.createUtility(this, SpeechConstant.APPID + "=5bea308f");
mIat = SpeechRecognizer.createRecognizer(this, null);
//设置mIat的参数
//表示是什么服务
mIat.setParameter(SpeechConstant.DOMAIN, "iat");
//设置语言
mIat.setParameter(SpeechConstant.LANGUAGE, "zh_cn");
//接受语言的类型
mIat.setParameter(SpeechConstant.ACCENT, "mandarin");
//使用什么样引擎
mIat.setParameter(SpeechConstant.ENGINE_TYPE, SpeechConstant.TYPE_CLOUD);
//语音识别接口监听:
mRecognizerLis=new RecognizerListener() {
@Override
public void onVolumeChanged(int i, byte[] bytes) {
}
@Override
public void onBeginOfSpeech() {
}
@Override
public void onEndOfSpeech() {
}
@Override //语音识别得到结果
public void onResult(RecognizerResult recognizerResult, boolean b) {
printResult(recognizerResult);
}
@Override
public void onError(SpeechError speechError) {
}
@Override
public void onEvent(int i, int i1, int i2, Bundle bundle) {
}
};
}
//解析语音识别结果成string
private void printResult(RecognizerResult results) {
String text = JsonParser.parseIatResult(results.getResultString());
String sn = null;
// 读取json结果中的sn字段
try {
JSONObject resultJson = new JSONObject(results.getResultString());
sn = resultJson.optString("sn");
} catch (JSONException e) {
e.printStackTrace();
}
mIatResults.put(sn, text);
StringBuffer resultBuffer = new StringBuffer();
for (String key : mIatResults.keySet()) {
resultBuffer.append(mIatResults.get(key));
}
//把消息发送给Unity场景中iFlytekASRController物体上的OnResult方法
UnityPlayer.UnitySendMessage("VoiceController", "GetVoiceResultFromAndroid", resultBuffer.toString());
}
// endregion
//#region 检测Unity和Android交互是否联通
public int beginTest(int a, int b){
//交互测试
return a+b;
}
public void connected(String str){
StringBuffer buffer = new StringBuffer(str);
buffer.append(" 连通成功了");
UnityPlayer.UnitySendMessage("VoiceController","TestConnectResult", buffer.toString());
}
//#endregion
}
注意:
1.初始化语音识别功能时使用的"APPID"需要科大讯飞官网上的产品id:
?所以在科大讯飞官网找到已经注册完成的应用,获取其id即可:
?2.语音识别功能需要获取设备的“RECORD_AUDIO"权限,否则无法正常识别
7.打包aar:
选中"voiceLib”,菜单栏“Build -》Make Module”
?
打包aar时遇到问题:
?解决办法:
不需要卸载"Build Tools 31.0.0"后重装。
之所以出现这个问题是由于缺失两个文件:dx.bat,? dx.jar
将文件夹内的“d8.bat”重命名为“dx.bat"
将子文件夹“lib”下的“d8.jar”修改为“dx.jar”即可
参考大佬详细解释链接:Android Studio error "Installed Build Tools revision 31.0.0 is corrupted" - Stack Overflow
两个文件重命名后打包完成:
?注意:在AndroidStudio中开发项目时需要设置其SDK,以及NDK路径。文末会附上需要用到的SDK, NDK下载链接,大家可以按需求取用
8.修改打包出来的aar文件:
打包完成后,在如下路径找到这两个文件:AndroidMainfest.xml,voiceLib-debug.aar
将这两个文件复制出来,新建一个文件夹用来存放这两个文件:
*** 修改aar包:
注意:这里不要把aar包解压,然后去修改里面的内容,最后重新压缩成aar。这样操作之后新生成的aar包是无法被Unity正常使用的:
选中该aar文件后,使用“360压缩”打开:
下面要执行的操作是将子文件夹libs中的“classes.jar”删掉,将aar包根目录中的“classes.jar”放到libs下
两个“classes.jar”只能留下一个,两个都存在时,Unity打包会报错
首先进入libs文件夹:选中“classes.jar"后直接删掉
然后选中根目录中的"classes.jar”,注意这里是无法直接拖动该文件到libs文件夹的。所以选中之后先复制,然后进入libs文件夹,粘贴即可
然后退回到aar根目录,选中根目录中的“classes.jar”,删除掉即可,两个classes.jar不可同时存在
删除后效果如下:
***修改aar包中的AndroidMainfest文件:
选中aar包中的AndroidMainfest.xml文件,打开:
将 android:label="voicelib" 这行代码删掉:这行代码用于显示Unity中打包出来的apk安装在真机上后显示的name。据说不删的话会与外部的“AndroidMainfest.xml"冲突,所以最好是删掉
?
?删掉之后的效果:
注意:这里不要把aar包解压,然后修改aar包内的AndroidMainfest.xml文件,直接在aar包内修改即可
如此“aar”包即修改完成
*** 修改包外的AndroidMainfest.xml文件:
这里修改package name为:不与aar内部的AndroidMainfest.xml一致即可。
如aar内部AndroidMainfest.xml的package name为“com.frank.voicelib”,因此这里修改外部的AndroidMainfest.xml为“com.frank.voicelib1”即可
PS:
1.外部AndroidMainfest.xml设置的package name需要与Unity中最后打包时PlayerSetting设置的package 一致,否则Unity会调用不到AndroidStudio中的方法
2.外部的 android:label="voicelib” 可以用于设置Unity中最后打包出来的apk安装在真机上显示的name。因此如果需要修改apk name可以直接在这里修改
PS: “label”属性控制的是Unity打包出来的apk在真机上安装完成后显示在手机上的apk名字,并不是Unity打包出来的apk名字或者PlayerSetting中的“Product Name”
默认情况下,如果没有外部导入的“AndroidMainfest.xml”文件,则使用Unity打包apk后安装在真机上显示出来的则是PlayerSetting中设置的“Product Name”
如此安卓端的所有操作都完成了
?二、Unity端:
在Unity中新建项目,创建“Plugins/Android”文件夹,将安卓端最后得到的两个文件导入进来:
在场景中新建GameObject “VoiceController",然后创建脚本“GameMgr.cs”,挂载在该“VoiceController”物体上:
编写脚本?GameMgr.cs:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class GameMgr : MonoBehaviour
{
private AndroidJavaObject jo;
//用于测试连通性
public Text testConnectText1, testConnectText2;
public Button testConnectedBtn;
public Button voiceBtn;
public Text permissionText, voiceResultText;
// Start is called before the first frame update
void Start()
{
AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
jo = jc.GetStatic<AndroidJavaObject>("currentActivity");
testConnectedBtn.onClick.AddListener(UnityAndAndroidTestConnect); //测试连通性
voiceBtn.onClick.AddListener(StarVoiceDetect);
}
public void StarVoiceDetect()
{
jo.Call("beginListen");
}
//权限获取结果
public void GetPermissionMsgFromAndroid(string msg)
{
permissionText.text = msg;
}
public void GetVoiceResultFromAndroid(string msg)
{
voiceResultText.text = msg;
}
#region 测试Unity和Android是否连接成功
public void UnityAndAndroidTestConnect()
{
testConnectText1.text = jo.Call<int>("beginTest", 222, 444).ToString();
jo.Call("connected", "Hello, today is a good day.");
}
public void TestConnectResult(string msg)
{
testConnectText2.text = msg;
}
#endregion
}
设置包名与aar外部的AndroidMainfest.xml中的包名一致,并根据aar项目的最低sdk版本设置“Minimum API Level”:
由于AS中的项目最低API版本为API-23,因此这里同样设置为“API level 23”。然后开始打包
打包完成后的apk在真机上运行效果如下:
PS:
打包时使用的SDK,NDK等文件下载链接:
SDK:?Unity_android_sdk.zip-Unity3D文档类资源-CSDN下载
NDK:?https://download.csdn.net/download/m0_47975736/33254055
源码工程地址:
包含AndroidStudio和Unity端的项目源码以及可以直接运行的APK文件:
https://download.csdn.net/download/m0_47975736/33253823
如此,在Unity接入科大讯飞语音SDK 安卓端就完成了。
|