[fea]
[唤醒 线程调整]
This commit is contained in:
yangyakun
2025-04-29 14:30:43 +08:00
parent b38fabdb63
commit e8b3e5a7c0
9 changed files with 391 additions and 325 deletions

View File

@@ -1,4 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.mogo.mgintelligent.speech" >
<uses-sdk tools:overrideLibrary="com.bun.miitmdid" />
</manifest>

View File

@@ -1,63 +1,23 @@
package com.mogo.mgintelligent.speech.iflytek
import android.Manifest
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.PackageManager
import android.media.AudioFormat
import android.media.AudioRecord
import android.media.MediaRecorder
import android.util.Log
import androidx.core.app.ActivityCompat
import com.iflytek.aikit.core.AiAudio
import com.iflytek.aikit.core.AiHandle
import com.iflytek.aikit.core.AiHelper
import com.iflytek.aikit.core.AiListener
import com.iflytek.aikit.core.AiRequest
import com.iflytek.aikit.core.AiResponse
import com.iflytek.aikit.core.AiStatus
import com.iflytek.aikit.core.BaseLibrary
import com.iflytek.aikit.core.CoreListener
import com.iflytek.aikit.core.ErrType
import com.iflytek.aikit.core.LogLvl
import com.mogo.commons.AbsMogoApplication
import com.mogo.eagle.core.function.call.setting.CallerRequestActivityHandleManager
import com.mogo.eagle.core.utilcode.mogo.logger.CallerLogger
import com.mogo.eagle.core.utilcode.util.FileUtils
import com.mogo.eagle.core.utilcode.util.ResourceUtils
import com.mogo.eagle.core.utilcode.util.ThreadUtils
import com.mogo.eagle.core.utilcode.util.Utils
import java.io.BufferedWriter
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.OutputStreamWriter
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.math.abs
import kotlin.math.log10
object WakeManager {
private val TAG = "WakeManager"
private const val ABILITYID: String = "e867a88f2"
private val weakupKey = "你好小智,小智小智,小智你好"
private val isEnd = AtomicBoolean(true)
//录音是否在进行
private val isRecording = AtomicBoolean(false)
private var aiHandle: AiHandle? = null
private var audioRecord: AudioRecord? = null
private var _wakeUpListener:WakeUpListener?=null
//录音缓冲区大小
private const val BUFFER_SIZE = 1280
private const val TAG = "WakeManager"
private val coreListener = CoreListener { type, code ->
Log.i(TAG, "core listener code:$code")
@@ -66,10 +26,10 @@ object WakeManager {
ErrType.AUTH -> {
if (code == 0) {
Log.i(TAG, "SDK授权成功")
WakeUpManager.startThread()
} else {
Log.i(TAG, "SDK授权失败授权码为:$code")
}
}
ErrType.HTTP -> Log.i(TAG, "SDK状态HTTP认证结果$code")
@@ -79,258 +39,6 @@ object WakeManager {
}
fun setWakeUpListener(wakeUpListener:WakeUpListener?){
this._wakeUpListener = wakeUpListener
}
fun startWakeUp(){
ThreadUtils.getIoPool().submit {
// 检测权限
val granted = ActivityCompat.checkSelfPermission(AbsMogoApplication.getApp(),
Manifest.permission.RECORD_AUDIO)
if(granted != PackageManager. PERMISSION_GRANTED){
CallerRequestActivityHandleManager.requestPermission(TAG,Manifest.permission.RECORD_AUDIO)
return@submit
}
AiHelper.getInst().registerListener(ABILITYID, edListener)
CallerLogger.d(TAG,"START")
val ret = start()
if (ret == 0) {
createAudioRecord() //创建录音器
isRecording.set(true)
audioRecord?.startRecording() //录音器启动录音
writeRecording(AiStatus.BEGIN)
}
}
}
fun stopWakeup(){
if(!isEnd.get()) {
writeRecording(AiStatus.END)
}
}
/**
* 能力监听回调
*/
private val edListener: AiListener = object : AiListener {
override fun onResult(handleID: Int, outputData: List<AiResponse>, usrContext: Any?) {
if (null != outputData && outputData.size > 0) {
for (i in outputData.indices) {
CallerLogger.d(TAG, "onResult:handleID:" + handleID + ":" + outputData[i].key)
val key = outputData[i].key //引擎结果的key
val bytes = outputData[i].value //识别结果
val result = String(bytes)
CallerLogger.d(TAG, "key=$key")
CallerLogger.d(TAG, "value=$result")
CallerLogger.d(TAG, "status=" + outputData[i].status)
if ((key == "func_wake_up" || key == "func_pre_wakeup")) {
CallerLogger.d(TAG,"$key: \n $result")
}
_wakeUpListener?.wakeupSuccess()
}
}
}
override fun onEvent(i: Int, i1: Int, list: List<AiResponse>, o: Any) {
CallerLogger.i(TAG, "onEvent:$i,event:$i1")
}
override fun onError(i: Int, i1: Int, s: String, o: Any) {
CallerLogger.d(TAG,"错误通知,能力执行终止,Ability $i ERROR::$s,err code:$i1")
}
}
private fun writeRecording(aiStatus:AiStatus){
/**
* 写入数据-录音方式
*/
val data = ByteArray(BUFFER_SIZE)
val read = audioRecord!!.read(data, 0, BUFFER_SIZE)
//平方和除以数据总长度,得到音量大小。
val volume = calculateVolume(data)
// CallerLogger.d(TAG,"当前分贝:${ abs(volume.toDouble())}")
if (AudioRecord.ERROR_INVALID_OPERATION != read) {
//处理录音数据
write(data, aiStatus) //送尾帧
}
if (AiStatus.END == aiStatus) {
if ( audioRecord != null) {
audioRecord!!.stop()
isRecording.set(false)
end()
}
} else {
if (isRecording.get()) {
writeRecording(AiStatus.CONTINUE) //调用write方法送音频数据给引擎
}
}
}
/**
* 结束会话
*/
private fun end() {
if (!isEnd.get()) {
val ret = AiHelper.getInst().end(aiHandle)
if (ret == 0) {
isEnd.set(true)
aiHandle = null
CallerLogger.d(TAG,"唤醒完成end $ret")
} else {
isEnd.set(false)
ThreadUtils.runOnUiThreadDelayed({
end()
CallerLogger.d(TAG,"关闭失败2s后重新广播唤醒完成end $ret")
},2_000)
CallerLogger.d(TAG,"唤醒完成end $ret")
}
}
}
/**
* 写入数据
*/
private fun write(part: ByteArray, status: AiStatus) {
if (isEnd.get()) {
return
}
val dataBuilder = AiRequest.builder()
var ret = 0
/**
* 送入音频需要标识音频的状态第一帧为起始帧status要传AiStatus.BEGIN,最后一帧为结束帧status要传AiStatus.END,其他为中间帧status要传AiStatus.CONTINUE
* 音频要求16bit16K单声道的pcm音频。
* 建议每次发送音频间隔40ms每次发送音频字节数为一帧音频大小的整数倍。
*/
val aiAudio = AiAudio.get("wav").data(part).status(status).valid()
dataBuilder.payload(aiAudio)
ret = AiHelper.getInst().write(dataBuilder.build(), aiHandle)
if (ret != 0) {
CallerLogger.d(TAG,"write失败$ret ")
}
}
/**
* 根据录音数据计算音量
*/
private fun calculateVolume(buffer: ByteArray): Int {
var sumVolume = 0.0
var avgVolume = 0.0
var volume = 0
var i = 0
while (i < buffer.size) {
val v1 = buffer[i].toInt() and 0xFF
val v2 = buffer[i + 1].toInt() and 0xFF
var temp = v1 + (v2 shl 8) // 小端
if (temp >= 0x8000) {
temp = 0xffff - temp
}
sumVolume += abs(temp.toDouble())
i += 2
}
avgVolume = sumVolume / buffer.size / 2
volume = (log10(1 + avgVolume) * 10).toInt()
return volume
}
/**
* 创建录音器
*/
@SuppressLint("MissingPermission")
private fun createAudioRecord() {
if (isRecording.get()) {
return
}
if (audioRecord == null) {
Log.d(TAG, "createAudioRecord")
audioRecord = AudioRecord(
MediaRecorder.AudioSource.MIC,
16000,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT,
BUFFER_SIZE
)
}
}
/**
* 开始会话
*/
private fun start(): Int {
if (!keyword2File()) {
CallerLogger.d(TAG,"唤醒词文件写入失败,请检查是否有读写权限。")
return -1
}
val customBuilder = AiRequest.builder()
customBuilder.customText("key_word", getIvwDir() + "/keyword.txt", 0)
var ret = AiHelper.getInst().loadData(ABILITYID, customBuilder.build())
if (ret != 0) {
CallerLogger.d(TAG,"open ivw loadData 失败:$ret")
return ret
}
CallerLogger.d(TAG,"open ivw loadData success$ret")
val indexs = intArrayOf(0)
ret = AiHelper.getInst().specifyDataSet(ABILITYID, "key_word", indexs) //从缓存中把个性化资源设置到引擎中
if (ret != 0) {
CallerLogger.d(TAG,"open ivw specifyDataSet 失败:$ret")
return ret
}
CallerLogger.d(TAG,"open ivw specifyDataSet success$ret")
val paramBuilder = AiRequest.builder()
paramBuilder.param("wdec_param_nCmThreshold", "0 0:800")
paramBuilder.param("gramLoad", true)
isEnd.set(false)
aiHandle = AiHelper.getInst().start(ABILITYID, paramBuilder.build(), null)
if (aiHandle?.code != 0) {
CallerLogger.d(TAG,"open ivw start失败${aiHandle?.getCode()}")
return aiHandle?.code ?:0
}
return 0
}
private fun keyword2File(): Boolean {
try {
val keywordFile: File = File(getIvwDir() + "/keyword.txt")
if (keywordFile.exists()) {
//强制清空内容
keywordFile.delete()
}
val binFile: File = File(getIvwDir() + "/keyword.bin")
if (binFile.exists()) {
binFile.delete()
}
var temp: String =weakupKey
if (temp.isEmpty()) {
temp = "你好小智"
}
val str = temp.replace("", ",")
val keywords = str.split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
if (!keywordFile.exists()) {
keywordFile.createNewFile()
}
val writer = OutputStreamWriter(
FileOutputStream(keywordFile),
"UTF-8"
)
val bufferedWriter = BufferedWriter(writer)
for (i in keywords.indices) {
bufferedWriter.write(keywords[i])
bufferedWriter.write(";")
bufferedWriter.newLine() //写入换行
}
bufferedWriter.close()
} catch (e: IOException) {
e.printStackTrace()
return false
}
return true
}
fun initWakeAi(context: Context) {
@@ -342,6 +50,7 @@ object WakeManager {
file.mkdirs()
}
AiHelper.getInst().setLogInfo(LogLvl.VERBOSE, 1, "${workDir}aeeLog.txt")
AiHelper.getInst().setLogMode(2)
//设定初始化参数
val params = BaseLibrary.Params.builder()
@@ -377,7 +86,7 @@ object WakeManager {
val assets = Utils.getApp().assets.list(dir)?.toMutableList()
val tartgetFile = File(targetDir)
if(tartgetFile.isDirectory){
tartgetFile.listFiles { dir, name ->
tartgetFile.listFiles { _, name ->
(assets?.contains(name)?:true).apply {
assets?.remove(name)
}
@@ -393,8 +102,4 @@ object WakeManager {
}
throw IllegalArgumentException("找不到 Application")
}
fun getIvwDir(): String {
return getWorkDir()+File.separator+"ivw"
}
}

View File

@@ -0,0 +1,369 @@
package com.mogo.mgintelligent.speech.iflytek
import android.Manifest
import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.media.AudioFormat
import android.media.AudioRecord
import android.media.MediaRecorder
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.util.Log
import androidx.core.app.ActivityCompat
import com.iflytek.aikit.core.AiAudio
import com.iflytek.aikit.core.AiHandle
import com.iflytek.aikit.core.AiHelper
import com.iflytek.aikit.core.AiListener
import com.iflytek.aikit.core.AiRequest
import com.iflytek.aikit.core.AiResponse
import com.iflytek.aikit.core.AiStatus
import com.mogo.commons.AbsMogoApplication
import com.mogo.eagle.core.function.call.setting.CallerRequestActivityHandleManager
import com.mogo.eagle.core.utilcode.mogo.logger.CallerLogger
import com.mogo.eagle.core.utilcode.util.ThreadUtils
import java.io.BufferedWriter
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.OutputStreamWriter
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.math.abs
import kotlin.math.log10
object WakeUpManager {
private const val START: Int = 0x0001
private const val WRITE_BY_RECORDING: Int = 0x0002
private const val END: Int = 0x0004
//录音缓冲区大小
private const val BUFFER_SIZE = 1280
private const val ABILITYID: String = "e867a88f2"
private const val weakupKey = "你好小智,小智小智,小智你好"
private val isEnd = AtomicBoolean(true)
//录音是否在进行
private val isRecording = AtomicBoolean(false)
private var aiHandle: AiHandle? = null
private const val TAG = "WakeUpManager"
private var mHandler: Handler? = null
private var audioRecord: AudioRecord? = null
private val mThread = Thread {
Looper.prepare()
mHandler = object : Handler(Looper.myLooper()!!) {
override fun handleMessage(msg: Message) {
super.handleMessage(msg)
when (msg.what) {
START -> {
/**
* 开始会话
*/
Log.d(TAG, "START")
val ret: Int = start()
if (ret == 0) {
val type = msg.arg1
if (type == WRITE_BY_RECORDING) {
createAudioRecord() //创建录音器
isRecording.set(true)
audioRecord?.startRecording() //录音器启动录音
}
mHandler?.removeCallbacksAndMessages(null) //清空消息队列
val writeMsg = Message()
writeMsg.what = type
writeMsg.obj = AiStatus.BEGIN
mHandler?.sendMessage(writeMsg) //调用write方法送音频数据给引擎
}
}
WRITE_BY_RECORDING -> {
/**
* 写入数据-录音方式
*/
val status = msg.obj as AiStatus
val data = ByteArray(BUFFER_SIZE)
val read: Int = audioRecord?.read(data, 0, BUFFER_SIZE)?:AudioRecord.ERROR_INVALID_OPERATION
//平方和除以数据总长度,得到音量大小。
val volume: Int = calculateVolume(data)
//CallerLogger.d(TAG,"当前分贝:${ abs(volume.toDouble())}")
if (AudioRecord.ERROR_INVALID_OPERATION != read) {
//处理录音数据
write(data, status) //送尾帧
}
if (AiStatus.END == status) {
if (audioRecord != null) {
audioRecord?.stop()
isRecording.set(false)
mHandler?.sendEmptyMessage(END)
}
} else {
if (isRecording.get()) {
val writeMsg = Message()
writeMsg.what = WRITE_BY_RECORDING
writeMsg.obj = AiStatus.CONTINUE
mHandler?.sendMessage(writeMsg) //调用write方法送音频数据给引擎
}
}
}
END -> {
/**
* 结束会话
*/
Log.d(TAG, "END")
end()
}
}
}
}
Looper.loop()
}
/**
* 能力监听回调
*/
private val edListener: AiListener = object : AiListener {
override fun onResult(handleID: Int, outputData: List<AiResponse>, usrContext: Any?) {
if (outputData.isNotEmpty()) {
for (i in outputData.indices) {
CallerLogger.d(TAG, "onResult:handleID:" + handleID + ":" + outputData[i].key)
val key = outputData[i].key //引擎结果的key
val bytes = outputData[i].value //识别结果
val result = String(bytes)
CallerLogger.d(TAG, "key=$key")
CallerLogger.d(TAG, "value=$result")
CallerLogger.d(TAG, "status=" + outputData[i].status)
if ((key == "func_wake_up" || key == "func_pre_wakeup")) {
CallerLogger.d(TAG,"$key: \n $result")
}
_wakeUpListener?.wakeupSuccess()
}
}
}
override fun onEvent(i: Int, i1: Int, list: List<AiResponse>, o: Any) {
CallerLogger.i(TAG, "onEvent:$i,event:$i1")
}
override fun onError(i: Int, i1: Int, s: String, o: Any) {
CallerLogger.d(TAG,"错误通知,能力执行终止,Ability $i ERROR::$s,err code:$i1")
}
}
private var _wakeUpListener:WakeUpListener?=null
fun setWakeUpListener(wakeUpListener:WakeUpListener?){
this._wakeUpListener = wakeUpListener
}
fun startThread(){
CallerLogger.d(TAG,"startThread")
mThread.start()
}
fun startWakeUp() {
if (isRecording.get() || !isEnd.get()) {
return
}
CallerLogger.d(TAG,"startWakeUp")
ThreadUtils.getIoPool().submit {
// 检测权限
val granted = ActivityCompat.checkSelfPermission(
AbsMogoApplication.getApp(),
Manifest.permission.RECORD_AUDIO)
if(granted != PackageManager. PERMISSION_GRANTED){
CallerRequestActivityHandleManager.requestPermission(TAG, Manifest.permission.RECORD_AUDIO)
return@submit
}
AiHelper.getInst().registerListener(ABILITYID, edListener)
val msg = Message()
msg.what = START
msg.arg1 = WRITE_BY_RECORDING
mHandler!!.sendMessage(msg)
}
}
fun stopWakeup(){
CallerLogger.d(TAG,"stopWakeup")
if(!isEnd.get()) {
val msg = Message()
msg.what = WRITE_BY_RECORDING
msg.obj = AiStatus.END
mHandler!!.sendMessage(msg)
}
}
/**
* 写入数据
*/
private fun write(part: ByteArray, status: AiStatus) {
if (isEnd.get()) {
return
}
val dataBuilder = AiRequest.builder()
/**
* 送入音频需要标识音频的状态第一帧为起始帧status要传AiStatus.BEGIN,最后一帧为结束帧status要传AiStatus.END,其他为中间帧status要传AiStatus.CONTINUE
* 音频要求16bit16K单声道的pcm音频。
* 建议每次发送音频间隔40ms每次发送音频字节数为一帧音频大小的整数倍。
*/
val aiAudio = AiAudio.get("wav").data(part).status(status).valid()
dataBuilder.payload(aiAudio)
val ret: Int = AiHelper.getInst().write(dataBuilder.build(), aiHandle)
if (ret != 0) {
CallerLogger.d(TAG,"write失败$ret ")
}
}
/**
* 创建录音器
*/
@SuppressLint("MissingPermission")
private fun createAudioRecord() {
if (isRecording.get()) {
return
}
if (audioRecord == null) {
Log.d(TAG, "createAudioRecord")
audioRecord = AudioRecord(
MediaRecorder.AudioSource.MIC,
16000,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT,
BUFFER_SIZE
)
}
}
/**
* 根据录音数据计算音量
*/
private fun calculateVolume(buffer: ByteArray): Int {
var sumVolume = 0.0
val volume: Int
var i = 0
while (i < buffer.size) {
val v1 = buffer[i].toInt() and 0xFF
val v2 = buffer[i + 1].toInt() and 0xFF
var temp = v1 + (v2 shl 8) // 小端
if (temp >= 0x8000) {
temp = 0xffff - temp
}
sumVolume += abs(temp.toDouble())
i += 2
}
val avgVolume: Double = sumVolume / buffer.size / 2
volume = (log10(1 + avgVolume) * 10).toInt()
return volume
}
/**
* 开始会话
*/
private fun start(): Int {
if (!keyword2File()) {
CallerLogger.d(TAG,"唤醒词文件写入失败,请检查是否有读写权限。")
return -1
}
val customBuilder = AiRequest.builder()
customBuilder.customText("key_word", getIvwDir() + "/keyword.txt", 0)
var ret = AiHelper.getInst().loadData(ABILITYID, customBuilder.build())
if (ret != 0) {
CallerLogger.d(TAG,"open ivw loadData 失败:$ret")
return ret
}
CallerLogger.d(TAG,"open ivw loadData success$ret")
val indexs = intArrayOf(0)
ret = AiHelper.getInst().specifyDataSet(ABILITYID, "key_word", indexs) //从缓存中把个性化资源设置到引擎中
if (ret != 0) {
CallerLogger.d(TAG,"open ivw specifyDataSet 失败:$ret")
return ret
}
CallerLogger.d(TAG,"open ivw specifyDataSet success$ret")
val paramBuilder = AiRequest.builder()
paramBuilder.param("wdec_param_nCmThreshold", "0 0:800")
paramBuilder.param("gramLoad", true)
isEnd.set(false)
aiHandle = AiHelper.getInst().start(ABILITYID, paramBuilder.build(), null)
if (aiHandle?.code != 0) {
CallerLogger.d(TAG,"open ivw start失败${aiHandle?.getCode()}")
return aiHandle?.code ?:0
}
return 0
}
/**
* 结束会话
*/
private fun end() {
if (!isEnd.get()) {
val ret = AiHelper.getInst().end(aiHandle)
if (ret == 0) {
isEnd.set(true)
aiHandle = null
CallerLogger.d(TAG,"唤醒完成end $ret")
} else {
isEnd.set(false)
ThreadUtils.runOnUiThreadDelayed({
end()
CallerLogger.d(TAG,"关闭失败2s后重新广播唤醒完成end $ret")
},2_000)
CallerLogger.d(TAG,"唤醒完成end $ret")
}
}
}
private fun keyword2File(): Boolean {
try {
val keywordFile = File(getIvwDir() + "/keyword.txt")
if (keywordFile.exists()) {
//强制清空内容
keywordFile.delete()
}
val binFile = File(getIvwDir() + "/keyword.bin")
if (binFile.exists()) {
binFile.delete()
}
var temp: String = weakupKey
if (temp.isEmpty()) {
temp = "你好小智"
}
val str = temp.replace("", ",")
val keywords = str.split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
if (!keywordFile.exists()) {
keywordFile.createNewFile()
}
val writer = OutputStreamWriter(
FileOutputStream(keywordFile),
"UTF-8"
)
val bufferedWriter = BufferedWriter(writer)
for (i in keywords.indices) {
bufferedWriter.write(keywords[i])
bufferedWriter.write(";")
bufferedWriter.newLine() //写入换行
}
bufferedWriter.close()
} catch (e: IOException) {
e.printStackTrace()
return false
}
return true
}
private fun getIvwDir(): String {
return WakeManager.getWorkDir() +File.separator+"ivw"
}
}