Android 集成 Unity3D 和 讯飞语音识别遇到的天坑
公司的一个税务智能咨询 APP《爱连塔可思》,主界面如图1所示。

图1
主界面上的数字人是一个 3D 动画,用 Unity3D 实现,按住对话功能通过讯飞语音识别将用户的语音转成文字,再发给服务端并获取答案。如图1所示。
APP 上线后本来没什么问题,但后来又要将主要功能集成到另一家公司的APP《大连税务》上,然后问题就出现了。
首先说一下《爱连塔可思》启动时先显示 logo Activity,再显示数字人 Activity。进入数字人 Activity 时, logo Activity 已经 finish 掉了,此时再按系统返回键就退出进程了,这是正常的逻辑,没什么问题,如图2所示。

图2
因为《大连税务》不需要《爱连塔可思》的 logo Activity,所以我们这边把《爱连塔可思》的工程分成两个模块,app 模块和 taxrobot 模块,logo Activity 在 app 模块中,数字人 Activity 在 taxrobot 模块中,这样就可以单独打包 taxrobot 模块,把输出的 aar 文件集成到《大连税务》APP上。《大连税务》是在它的主 Activity 上点击一个图标进入数字人 Activity,按返回键就回到主 Activity。如图3所示。

图3
可是实际效果却是,按返回键后《大连税务》闪退了。通过调查发现,这个闪退其实是在离开数字人 Activity 时调用了 Unity3D 的释放资源的方法,而这个释放资源的方法又调用了 exit 方法退出了整个 APP,也就是说,这并不是闪退,而是 Unity3D 的正常逻辑。那怎么解决呢?一开始想在离开时不调用释放资源的方法,结果发现不释放的话,下次再进入数字人 Activity 时,数字人创建失败了,而且理论上不释放还可能造成内存泄露问题,所以必须释放。思前想后,只有一个办法:双进程,就是将 主 Activity 在一个进程中(以下简称”主进程“),数字人 Activity在另一个进程中(以下简称”数字人进程”),这样在离开数字人 Activity 时,退出的是数字人进程,不会影响到主进程。因为 Android 是支持一个 APP 运行在多个进程中的。所以,修改方案是在 AndroidManifest.xml 中的数字人 Activity 的描述中增加 android:process=".unity",如下所示:
<activity
android:name="com.xx.taxrobot.TaxRobotActivity"
android:process=".unity" />
这样,主 Activity 运行在 com.xx.dltax 进程(主进程)中,而 数字人 Activity 运行在 com.xx.dltax.unity 进程(数字人进程)中。但是这么修改又遇到另一个问题,就是在数字人 Activity 中按下“按住对话”后,无论说什么,APP 都没有任何反应。调查发现,是因为讯飞语音识别的相关对象创建失败了,经过大量调试和调查仍然找不到原因,所以只能采用规避的办法。因为在主进程中是可以成功创建讯飞语音识别的,所以修改方案是,在 taxrobot 模块中增加 Application 类,在 Application 类中创建一个 Service,在 Service 中创建讯飞语音识别并提供相应的对外接口方法。因为 Application 是运行在主进程中的且先于 Activity 运行,这样创建的 Service 也在主进程中,讯飞语音识别当然也是在主进程中。现在又遇到另一个问题,数字人 Activity 的“按住对话”和讯飞语音识别不在同一进程中,怎么进行语音识别?答案就是进程通信。Android 中的进程通信有多种方式,这里采用简单的 Messenger,整个通信过程如图4所示。

图4
以上修改方案的主要代码如下所示:
// 语音工具
class SpeechUtil: Service(), Callback, RecognizerListener {
companion object {
const val KEY_ASR_TEXT = "asrText" // 键:语音识别的文字
const val KEY_MESSENGER = "messenger" // 键:信使
const val MSG_GET_ASR_RESULT = 1000 // 得到语音识别结果消息
const val MSG_START_LISTENING = 1001 // 开始语音识别消息
const val MSG_STOP_LISTENING = 1002 // 停止语音识别消息
private lateinit var sClientMessenger: Messenger // 客户端信使
private lateinit var sServerMessenger: Messenger // 服务器信使
/**
* 初始化。
* @param context 设备环境
* @param callback 回调
*/
fun init(context: Context, callback: Callback?) {
val intent = Intent(context, SpeechUtil::class.java)
if (callback != null) {
val handler = Handler(Looper.getMainLooper(), callback)
val messenger = Messenger(handler)
intent.putExtra(KEY_MESSENGER, messenger)
val connection = object: ServiceConnection {
override fun onServiceConnected(componentName: ComponentName, iBinder: IBinder) {
sServerMessenger = Messenger(iBinder)
}
override fun onServiceDisconnected(componentName: ComponentName) {
}
}
try {
Thread.sleep(1000)
} catch (e: Exception) {
e.printStackTrace()
}
context.bindService(intent, connection, 0)
} else {
context.startService(intent)
}
}
/**
* 从 Json 字符串中解析语音识别结果。
* @param json Json 字符串
* @return 语音识别结果
*/
fun parseIatResult(json: String): String {
val stringBuilder = StringBuilder()
try {
val tokener = JSONTokener(json)
val joResult = JSONObject(tokener)
val words = joResult.getJSONArray("ws")
for (i in 0 ..< words.length()) {
val items = words.getJSONObject(i).getJSONArray("cw")
val obj = items.getJSONObject(0)
var word = obj.getString("w")
word = word.replaceFirst(",".toRegex(), "")
.replaceFirst("。".toRegex(), "")
.replaceFirst("!".toRegex(), "")
.replaceFirst("嗯".toRegex(), "")
if (word.lastIndexOf("。") == word.length - 1) {
word = word.substring(0, word.length - 2)
}
stringBuilder.append(word)
}
} catch (e: Exception) {
e.printStackTrace()
}
return stringBuilder.toString()
}
/**
* 开始语音识别。
*/
fun startListening() {
val message = Message.obtain(null, MSG_START_LISTENING)
try {
sServerMessenger.send(message)
} catch (e: Exception) {
e.printStackTrace()
}
}
/**
* 停止语音识别。
*/
fun stopListening() {
val message = Message.obtain(null, MSG_STOP_LISTENING)
try {
sServerMessenger.send(message)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
private lateinit var mSpeechRecognizer: SpeechRecognizer // 语音识别者
/**
* 处理消息。
* @param msg 消息
* @return 消息在处被处理则返回 true,否则返回 false
*/
override fun handleMessage(msg: Message): Boolean {
if (msg.what == MSG_START_LISTENING) {
mSpeechRecognizer.startListening(this)
} else if (msg.what == MSG_STOP_LISTENING) {
mSpeechRecognizer.stopListening()
}
return false
}
/**
* 绑定事件的响应方法。
* @param intent 意图
* @return 绑定者
*/
override fun onBind(intent: Intent): IBinder? {
sClientMessenger = intent.getParcelableExtra<Messenger>(KEY_MESSENGER)!!
val handler = Handler(Looper.getMainLooper(), this)
return Messenger(handler).binder
}
/**
* 创建事件的响应方法。
*/
override fun onCreate() {
// 创建语音识别者
mSpeechRecognizer = SpeechRecognizer.createRecognizer(this, InitListener { i: Int ->
mSpeechRecognizer.setParameter(SpeechConstant.PARAMS, null)
mSpeechRecognizer.setParameter(SpeechConstant.ENGINE_TYPE, SpeechConstant.TYPE_CLOUD)
mSpeechRecognizer.setParameter(SpeechConstant.TEXT_ENCODING, StandardCharsets.UTF_8.name())
mSpeechRecognizer.setParameter(SpeechConstant.RESULT_TYPE, "json")
mSpeechRecognizer.setParameter(SpeechConstant.ACCENT, "mandarin")
mSpeechRecognizer.setParameter(SpeechConstant.VAD_BOS, "4000")
mSpeechRecognizer.setParameter(SpeechConstant.VAD_EOS, "4000") // 用户停止说话多长时间内即认为不再输入
mSpeechRecognizer.setParameter(SpeechConstant.ASR_PTT, "1") // 设置为"0"返回结果无标点,设置为"1"返回结果有标点
mSpeechRecognizer.setParameter(SpeechConstant.AUDIO_FORMAT, "wav")
mSpeechRecognizer.setParameter(SpeechConstant.LANGUAGE, "zh_CN")
})
}
/**
* 语音识别结束事件的响应方法。
*/
override fun onEndOfSpeech() {
mSpeechRecognizer.stopListening()
}
/**
* 获取到语音识别结果的响应方法。
* @param result 语音识别结果
* @param b ?
*/
override fun onResult(result: RecognizerResult, b: Boolean) {
mSpeechRecognizer.stopListening()
val message = Message.obtain()
message.what = MSG_GET_ASR_RESULT
val data = Bundle()
data.putString(KEY_ASR_TEXT, result.resultString)
message.data = data
try {
sClientMessenger.send(message)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
总结一下,主要有3点:
1.Unity3D的 Activity 退出时,整个 APP 的进程也会退出,所以要返回上一个 Activity 就得采用双进程。
2.在 Unity3D 的 进程里创建讯飞语音识别会失败,原因不明,所以只能在主进程里创建。
3.主进程和 Unity3D 进程之间通过 Messenger 通信。
全文完。

浙公网安备 33010602011771号