Android 集成 Unity3D 和 讯飞语音识别遇到的天坑

公司的一个税务智能咨询 APP《爱连塔可思》,主界面如图1所示。
1
图1

主界面上的数字人是一个 3D 动画,用 Unity3D 实现,按住对话功能通过讯飞语音识别将用户的语音转成文字,再发给服务端并获取答案。如图1所示。
APP 上线后本来没什么问题,但后来又要将主要功能集成到另一家公司的APP《大连税务》上,然后问题就出现了。

首先说一下《爱连塔可思》启动时先显示 logo Activity,再显示数字人 Activity。进入数字人 Activity 时, logo Activity 已经 finish 掉了,此时再按系统返回键就退出进程了,这是正常的逻辑,没什么问题,如图2所示。
2
图2

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

全文完。

posted @ 2025-07-07 17:19  安联酋长  阅读(13)  评论(0)    收藏  举报