diff --git a/android/app/build.gradle b/android/app/build.gradle index bf89af0..08e4653 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -101,8 +101,6 @@ flutter { } dependencies { - // sing sound - implementation 'com.singsound.library:evaluating:2.1.9' implementation "com.google.code.gson:gson:2.10" // 基础依赖包,必须要依赖 implementation 'com.geyifeng.immersionbar:immersionbar:3.2.2' diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro index 5660f35..7f9e64a 100644 --- a/android/app/proguard-rules.pro +++ b/android/app/proguard-rules.pro @@ -20,12 +20,6 @@ # hide the original source file name. #-renamesourcefileattribute SourceFile -# 先声混淆代码 --keep class com.tt.** { *; } --keep class com.xs.** { *; } --keep interface com.xs.** { *; } --keep enum com.xs.** { *; } - # 友盟混淆 -keep class com.umeng.** { *; } diff --git a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/MainActivity.kt b/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/MainActivity.kt index 88c9499..ab3c3b6 100644 --- a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/MainActivity.kt +++ b/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/MainActivity.kt @@ -9,7 +9,6 @@ import androidx.core.view.WindowInsetsControllerCompat import com.gyf.immersionbar.BarHide import com.gyf.immersionbar.ImmersionBar import com.kouyuxingqiu.wow_english.methodChannels.GameMethodChannel -import com.kouyuxingqiu.wow_english.methodChannels.SingSoungMethodChannel import com.umeng.commonsdk.UMConfigure import com.umeng.umcrash.UMCrash import io.flutter.embedding.android.FlutterActivity @@ -25,7 +24,6 @@ class MainActivity : FlutterActivity() { //隐藏状态栏和导航栏 ImmersionBar.with(this).hideBar(BarHide.FLAG_HIDE_BAR).init() flutterEngine?.let { - SingSoungMethodChannel(this, it) GameMethodChannel(this, it) } } diff --git a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/methodChannels/SingSoungMethodChannel.kt b/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/methodChannels/SingSoungMethodChannel.kt deleted file mode 100644 index db16ab7..0000000 --- a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/methodChannels/SingSoungMethodChannel.kt +++ /dev/null @@ -1,103 +0,0 @@ -package com.kouyuxingqiu.wow_english.methodChannels - -import android.util.Log -import com.kouyuxingqiu.wow_english.singsound.SingEngineHelper -import com.kouyuxingqiu.wow_english.singsound.SingEngineHelper.init -import com.kouyuxingqiu.wow_english.singsound.SingEngineLifecycles -import com.kouyuxingqiu.wow_english.util.GlobalHandler -import io.flutter.embedding.android.FlutterActivity -import io.flutter.embedding.engine.FlutterEngine -import io.flutter.plugin.common.MethodChannel -import java.lang.ref.WeakReference - -/** - * @author: stay - * @date: 2023/6/27 00:32 - * @description: - */ -class SingSoungMethodChannel(activity: FlutterActivity, flutterEngine: FlutterEngine): SingEngineLifecycles.OnSingEngineAdapter() { - private var methodChannel: MethodChannel? = null - private val TAG = "SingSoungMethodChannel" - - companion object { - var channel: WeakReference? = null - - fun invokeMethod(method: String, arguments: Any?) { - channel?.get()?.methodChannel?.invokeMethod(method, arguments) - } - } - - init { - // name需与flutter端一致 - methodChannel = - MethodChannel( - flutterEngine.dartExecutor.binaryMessenger, - "wow_english/sing_sound_method_channel" - ) - init(activity) - methodChannel?.setMethodCallHandler { call, result -> - Log.d(TAG, "SingSoungMethodChannel CALL=${call.method} ${call.arguments}") - when (call.method) { - "initVoiceSdk" -> { - - } - "startVoice" -> { - val paramMap = call.arguments as HashMap - paramMap["word"]?.let { SingEngineHelper.startRecord(it) } - } - "stopVoice" -> { - SingEngineHelper.stopRecord() - } - "startLocalVoice" -> { - val paramMap = call.arguments as HashMap - paramMap["voicePath"]?.let { voiceFilePath -> - paramMap["word"]?.let { evaluateContent -> - SingEngineHelper.evaluate(voiceFilePath, evaluateContent) } - } - - } - "cancelVoice" -> { - SingEngineHelper.cancel() - } - else -> { - result.notImplemented() - } - } - - } - channel = WeakReference(this) - - SingEngineHelper.addOnResultListener(this) - } - - override fun onResult(map: Map, evalType: Int?) { - //先声回调在子线程,需要切换到主线程 - GlobalHandler.runOnMainThread { - invokeMethod("voiceResult", map) - } - } - - override fun onRecordFail(code: Int, message: String) { - GlobalHandler.runOnMainThread { - invokeMethod("voiceFail", mapOf("code" to code, "message" to message)) - } - } - - override fun onRecordBegin() { - GlobalHandler.runOnMainThread { - invokeMethod("voiceStart", null) - } - } - - override fun onRecordStop() { - GlobalHandler.runOnMainThread { - invokeMethod("voiceEnd", null) - } - } - - override fun onCancel() { - GlobalHandler.runOnMainThread { - invokeMethod("voiceCancel", null) - } - } -} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/BaseCloudFragment.kt b/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/BaseCloudFragment.kt deleted file mode 100644 index 6658320..0000000 --- a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/BaseCloudFragment.kt +++ /dev/null @@ -1,273 +0,0 @@ -//package com.ishow.english.module.lesson.question.sound -// -//import android.graphics.Color -//import android.os.Bundle -//import android.os.Handler -//import android.os.Looper -//import android.text.SpannableString -//import android.text.TextUtils -//import com.daimajia.androidanimations.library.YoYo -//import com.ishow.english.common.Constant -//import com.ishow.english.module.lesson.BaseLessonFragment -//import com.ishow.english.module.lesson.BaseLessonStrategy -//import com.ishow.english.module.lesson.LessonMode -//import com.ishow.english.module.lesson.LessonType -//import com.ishow.parent.module.question.sound.bean.BaseResultModel -//import com.ishow.english.module.lesson.question.sound.config.EvalTargetType -//import com.ishow.parent.module.question.sound.util.VoiceSpannableString -//import com.ishow.parent.audio.Audio -//import com.ishow.english.utils.CountDownHelper -//import com.ishow.english.utils.IWarningStateListenerAdapter -//import com.ishow.english.utils.WarnToneManager -//import com.jiongbull.Log.Log -//import com.perfect.utils.StringUtils -//import org.json.JSONObject -// -// -///** -// * @author stay -// * @date 2019-1-7 -// * @describe 语音测评基类 -// */ -//abstract class BaseCloudFragment : BaseLessonFragment() { -// -// val TAG = "BaseCloudFragment" -// var rope: YoYo.YoYoString? = null -// val mSingEngineLifecycles: SingEngineLifecycles -// var mHandler: Handler -// -// init { -// mHandler = Handler(Looper.getMainLooper()) -// -// mSingEngineLifecycles = object : SingEngineLifecycles.OnSingEngineAdapter() { -// override fun onResult(result: JSONObject, evalType: Int?) { -// Log.d(TAG, "evalType = $evalType result = $result") -// val resultModel = SingEngineManager.get().parseResult(result, evalType) -// val originText = StringUtils.replaceChinesePunctuationToEnglish(resultModel.originText) -// var resutlSpan = VoiceSpannableString(getContext(), originText) -// activity?.runOnUiThread { -// Log.w(Constant.TAG_THREADID, "onResult = ${android.os.Process.myTid()}") -// if (!TextUtils.isEmpty(originText)) { -// if (activity != null) { -// when (evalType) { -// EvalTargetType.SENTENCE -> { -// var startIndex = 0 -// for (r in resultModel.scores) { -// Log.e(TAG, "startIndex = $startIndex r.char = ${r.char}") -// startIndex = originText.indexOf(r.char, startIndex) -// Log.e(TAG, "k = ${r.char} startIndex = $startIndex score = ${r.score}") -// if (startIndex == -1) { //异常情况,或者音标文字识别不了 -// } else { -// resutlSpan.first(r.char, startIndex).textColor(parseScoreToColor(r.score)) -// } -// startIndex += r.char.length -// } -// } -// EvalTargetType.WORD -> { -// Log.e(TAG, "WORD = ${resultModel.originText} ${resultModel.score}") -// resutlSpan.all(originText).textColor(parseScoreToColor(resultModel.score)) -// } -// EvalTargetType.ALPHA -> { -// if (originText.trim() == "abc") { -// resutlSpan = VoiceSpannableString(getContext(), "/ɔr/") -// resutlSpan.all("/ɔr/").textColor(parseScoreToColor(resultModel.score)) -// } else { -// Log.e(TAG, "ALPHA = ${resultModel.originText} ${resultModel.score}") -// resutlSpan.all(originText).textColor(parseScoreToColor(resultModel.score)) -// } -// } -// } -// } -// -// mStrategy.decreaseChance() -// -// val delay = if (mLessonExtroBundle.lessonMode == LessonMode.EXAM) 400L else 1000L -// mHandler.postDelayed({ -// // // 计分 -// if (mLessonPagePacket.type != LessonType.VOICE_JSBY) { // 角色扮演不需要每句都打分 -// lessonEvaluat(resultModel.score.toInt()) -// } -// -// onSoundResult(resutlSpan, resultModel) -// changeBottomSheetLayoutState(false) -// if (mStrategy.needNotifyResult) { // 除了角色扮演和测评课,其余都为true -// // 延迟1s是为了等待RecordRippleView结束提示音以及zoomout动画 -// -// var warnId: Int? = null -// if (mLessonPagePacket.type == LessonType.STATEMENT) { // 题干单独处理 -// if (mLessonPagePacket.score >= Constant.VOICE_NICE_SCORE) { -// warnId = WarnToneManager.RECORD_NICE -// } else { -// if (mStrategy.needPlayBack) { -// SingEngineManager.get().playBack() -// this@BaseCloudFragment.onPlayBack() -// } else { -// this@BaseCloudFragment.onRecordPlayOver() -// } -// } -// } else { // 非题干 -// if (mLessonPagePacket.score >= Constant.VOICE_SUCCESS_SCORE) { -// warnId = WarnToneManager.RIGHT -// } else { -// warnId = WarnToneManager.WRONG -// } -// } -// -// if (warnId != null) { -// WarnToneManager.play(warnId, object : IWarningStateListenerAdapter() { -// override fun onStart(audio: Audio?) { -// if (mLessonPagePacket.type == LessonType.STATEMENT) { -// onStatementNice() -// } -// } -// -// override fun onCompleted(audio: Audio?) { -// if (mLessonPagePacket.type == LessonType.STATEMENT) { // 题干播放完nice后 -// SingEngineManager.get().playBack() -// this@BaseCloudFragment.onPlayBack() -// } else { -// playCoinSound().subscribe { -// if (mStrategy.needPlayBack) { -// SingEngineManager.get().playBack() -// this@BaseCloudFragment.onPlayBack() -// } else { // gaming模式和exam模式不需要播放录音 -// if (!mStrategy.checkAnyChance()) { -// exitFragmentDelay() -// } -// } -// } -// } -// } -// }) -// } -// } else { -// if (mLessonExtroBundle.lessonMode == LessonMode.EXAM) { -// exitFragmentDelay() -// } -// } -// }, delay) -// } -// } -// } -// -// override fun onRecordBegin() { -// startCountDown() -// this@BaseCloudFragment.onRecordBegin() -// } -// -// override fun onRecordStop() { -// activity?.runOnUiThread { -// Log.w(Constant.TAG_THREADID, "onRecordStop = ${android.os.Process.myTid()}") -// mLessonPagePacket.voiceRecordPath = SingEngineManager.get().getWavePath() -//// if (mIsOverTime) { -//// if (mLessonPagePacket.type != LessonType.VOICE_JSBY) { -//// nextPage() -//// } -//// } else { -//// // 如果没有超时,手动掐断计时器 -//// CountDownHelper.get().stop() -//// this@BaseCloudFragment.onRecordStop() -//// } -// if (mStrategy.needCountDown) { -// CountDownHelper.get().stop() -// } -// this@BaseCloudFragment.onRecordStop() -// } -// } -// -// override fun onRecordPlayOver() { -// Log.w(Constant.TAG_THREADID, "onRecordPlayOver = ${android.os.Process.myTid()}") -// activity?.runOnUiThread { -// changeBottomSheetLayoutState(false) -// this@BaseCloudFragment.onRecordPlayOver() -// } -// } -// } -// } -// -// override fun onActivityCreated(savedInstanceState: Bundle?) { -// super.onActivityCreated(savedInstanceState) -// SingEngineManager.get().addOnResultListener(mSingEngineLifecycles) -// } -// -// override fun initConfig(): BaseLessonStrategy { -// if (mLessonExtroBundle.lessonMode == LessonMode.GAMING) { -// return BaseLessonStrategy.GameingLessonStrategy(mLessonPagePacket) -// } else if (mLessonExtroBundle.lessonMode == LessonMode.EXAM) { -// return BaseLessonStrategy.ExamLessonStrategy(mLessonPagePacket) -// } else { -// return BaseLessonStrategy.VoiceLessonStrategy() -// } -// } -// -// /** -// * 流程开始 -// */ -// open fun action() { -// -// } -// -// /** -// * 录音开始 -// */ -// open fun onRecordBegin() { -// -// } -// -// /** -// * 录音结束(经测试该方法在子线程) -// */ -// open fun onRecordStop() { -// -// } -// -// /** -// * 开始播放录音 -// */ -// open fun onPlayBack() { -// -// } -// -// /** -// * 结束播放录音 -// */ -// open fun onRecordPlayOver() { -// -// } -// -// /** -// * coin动画 -// */ -// open fun onStatementNice() { -// -// } -// -// /** -// * @param spannableString 根据评测结果返回的带颜色的string -// * @param resultModel 评测结果解析后的数据 -// * 评测结束并解析完成 -// */ -// open fun onSoundResult(spannableString: SpannableString, resultModel: BaseResultModel) { -// -// } -// -// -// override fun onDestroy() { -// super.onDestroy() -// SingEngineManager.get().removeOnResultListener(mSingEngineLifecycles) -// mHandler.removeCallbacksAndMessages(null) -// } -//} -// -///** -// * 根据分数给不同文字上色 -// */ -//fun parseScoreToColor(score: Double): Int { -// if (score < 60) { -// return Color.parseColor("#FF3B30") -// } else if (score in 60f..80f) { -// return Color.parseColor("#33373F") -// } else { -// return Color.parseColor("#0ABB08") -// } -//} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/OnSingEngineLifecycles.kt b/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/OnSingEngineLifecycles.kt deleted file mode 100644 index acf9b3d..0000000 --- a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/OnSingEngineLifecycles.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.kouyuxingqiu.wow_english.singsound - -import com.kouyuxingqiu.wow_english.singsound.config.EvalTargetType -import org.json.JSONObject - -interface SingEngineLifecycles { - // 录音开始 - fun onRecordBegin() - - // 录音结束 - fun onRecordStop() - - // 播放录音结束 - fun onRecordPlayOver() - - // 评测完成 - fun onResult(map: Map, @EvalTargetType evalType: Int? = EvalTargetType.SENTENCE) - - // 取消评测 - fun onCancel() - - /** - * 评测失败 - * @param code 失败错误码 - * @param message 失败错误信息 - */ - fun onRecordFail(code: Int, message: String) - - - abstract class OnSingEngineAdapter : SingEngineLifecycles { - override fun onRecordBegin() { - - } - - override fun onRecordStop() { - - } - - override fun onRecordPlayOver() { - - } - - override fun onResult(map: Map, @EvalTargetType evalType: Int?) { - - } - - override fun onCancel() { - - } - - override fun onRecordFail(code: Int, message: String) { - - } - } -} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/ParseDataHelper.kt b/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/ParseDataHelper.kt deleted file mode 100644 index 248b644..0000000 --- a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/ParseDataHelper.kt +++ /dev/null @@ -1,106 +0,0 @@ -package com.kouyuxingqiu.wow_english.singsound - -import android.util.Log -import com.google.gson.Gson -import com.kouyuxingqiu.wow_english.singsound.bean.BaseResultModel -import com.kouyuxingqiu.wow_english.singsound.bean.RealtimeResultEntity -import com.kouyuxingqiu.wow_english.singsound.config.EvalTargetType -import com.kouyuxingqiu.wow_english.singsound.util.JsonUtils -import org.json.JSONObject - -/** - * @author: stay - * @date: 2020/9/1 11:49 - * @description: - */ - -/** - * 解析评测结果 - * @param result 评测结果 - * @param evalType 评测类型 - */ -fun parseResult(result: JSONObject, evalType: Int? = EvalTargetType.SENTENCE): BaseResultModel { - val resultModel = BaseResultModel() - try { - if (result.has("result")) { - resultModel.originText = JsonUtils.getString(result, "refText") - when (evalType) { - EvalTargetType.SENTENCE -> { - resultModel.originText = resultModel.originText?.replace("’", "'") // 统一英文符号 - val resultJ = JsonUtils.getJsonObject(result, "result") - - if (resultJ != null) { - resultModel.score = JsonUtils.getDouble(resultJ, "overall") - resultModel.pronounce = JsonUtils.getDouble(resultJ, "pron") - resultModel.fluency = - JsonUtils.getJsonObject(resultJ, "fluency").getDouble("overall") - // 遍历所有词汇 - val detailsA = JsonUtils.getJsonArray(resultJ, "details") - for (i in 0 until detailsA.length()) { - val detailsWords = detailsA.getJSONObject(i) - var charStr = JsonUtils.getString(detailsWords, "char") - - // 过滤掉多余符号 - charStr = charStr.replace(".", "") - charStr = charStr.replace(",", "") - charStr = charStr.replace("’", "'") // 统一英文符号 - - resultModel.scores.add( - BaseResultModel.SingleResultModel( - charStr, - JsonUtils.getDouble(detailsWords, "score") - ) - ) - } - } - } - EvalTargetType.ALPHA -> { - val result_JsonObject = result.optJSONObject("result") - if (result_JsonObject != null) { - resultModel.score = result_JsonObject.getDouble("overall") - } - } - EvalTargetType.WORD -> { - val resultW = JsonUtils.getJsonObject(result, "result") - if (resultW != null) { - resultModel.score = JsonUtils.getDouble(resultW, "overall") - } - } - } - } - } catch (e: Exception) { - e.printStackTrace() - Log.e("parseResult", e.message.toString()) - } - return resultModel -} - -/** - * 解析Realtime结果 - */ -fun parseResult4Real(result: JSONObject): RealtimeResultEntity? { - var resultModel: RealtimeResultEntity? = null - try { - if (result.has("result")) { - val resultJson = JsonUtils.getString(result, "result") - resultModel = Gson().fromJson(resultJson, RealtimeResultEntity::class.java) - } - } catch (e: Exception) { - e.printStackTrace() - Log.e("parseResult4Real", e.message.toString()) - } - return resultModel -} - - -fun filterAllPunctuation(s: String): String? { - return s.replace( - "[`qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM~!@#$%^&*()+=|{}':;',\\[\\].<>/?~!@#¥%……& amp;*()——+|{}【】‘;:”“’。,、?|-]".toRegex(), - "" - ) - - var str = - ",.!,,D_NAME。!;‘’”“**dfs #$%^&()-+1431221\"\"中 国123漢字かどうかのjavaを決定" - str = str.replace("[\\pP\\pS]".toRegex(), "") - println(str) -} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/SingEngineHelper.kt b/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/SingEngineHelper.kt deleted file mode 100644 index 5ebf180..0000000 --- a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/SingEngineHelper.kt +++ /dev/null @@ -1,445 +0,0 @@ -package com.kouyuxingqiu.wow_english.singsound - -import android.content.Context -import android.util.Log -import com.constraint.CoreProvideTypeEnum -import com.constraint.ResultBody -import com.google.gson.Gson -import com.kouyuxingqiu.wow_english.singsound.config.EvalTargetType -import com.kouyuxingqiu.wow_english.singsound.config.SingSoundConfig -import com.kouyuxingqiu.wow_english.singsound.config.VoiceConfig -import com.kouyuxingqiu.wow_english.singsound.config.WordConfig -import com.kouyuxingqiu.wow_english.singsound.util.JsonUtils.toMap -import com.xs.SingEngine -import com.xs.impl.AudioErrorCallback -import com.xs.impl.EvalReturnRequestIdCallback -import com.xs.impl.OnRealTimeResultListener -import com.xs.utils.AiUtil -import org.json.JSONObject -import java.util.* - - -object SingEngineHelper : - AudioErrorCallback, EvalReturnRequestIdCallback, OnRealTimeResultListener { - - private val TAG = "SingEngineManager" - private var mSingEngine: SingEngine? = null - - /** - * 所有逻辑回调集合 - */ - private var mListeners: MutableList? = null - - /** - * 是否初始化完成 - */ - private var mIsReady = false - - /** - * 是否取消测评 - */ - private var mCanceled = false - - /** - * 音标转换表 - */ - private val mAlphaMap = LinkedHashMap() - - - private var mCurEvalType: Int? = EvalTargetType.SENTENCE - - /** - * 目前建议在应用入口初始化 - */ - fun init(context: Context) { - mListeners = mutableListOf() - if (mSingEngine == null) { - mSingEngine = SingEngine.newInstance(context) - Thread { - try { - mSingEngine?.run { - // 设置测评结果监听器 - setListener(this@SingEngineHelper) - // 设置录音器初始化错误的回调 - setAudioErrorCallback(this@SingEngineHelper) - setEvalReturnRequestIdCallback(this@SingEngineHelper) -// // 设置音频格式 -// setAudioType(AudioTypeEnum.WAV) - // 设置引擎类型。引擎类型(在线CLOUD、 离线NATIVE、混合AUTO),默认使用在线引擎。 - setServerType(CoreProvideTypeEnum.CLOUD) - // 设置log日志级别 - setLogLevel(4) - // 禁用实时音量返回 - disableVolume() - // 设置录音音频路径 - wavPath = AiUtil.getFilesDir(context).path + "/userdata/sound_record/" - // 设置是否开启 VAD 功能 -// setOpenVad(true, "vad.0.1.bin") - //setOpenVad(false, null); - // 设置 VAD 前置超时时间 - setFrontVadTime(3000) -// setServerTimeout(10000) - // 开启错误日志保存到本地,发生错误时文件中会保存到android/data/包名/files/SSError.txt中 -// setOpenWriteLog(true) - // 设置在线服务器地址和账号 - setServerAPI("wss://api.cloud.ssapi.cn") -// // 设置评测语言(针对离线评测) -// setOffLineSource(OffLineSourceEnum.SOURCE_EN) - // 设置引擎初始化参数 - setNewCfg( - buildInitJson( - SingSoundConfig.APPKEY, - SingSoundConfig.SECERTKEY - ) - ) - // 引擎初始化 - createEngine("1") - - Log.w(TAG, "createEngine") - } - - getSymbolsMap() - } catch (e: Exception) { - e.printStackTrace() - } - }.start() - } - } - - // 开始语音评测(先声录音+评测) - fun startRecord(originText: String, @EvalTargetType evalTargetType: Int? = EvalTargetType.SENTENCE, userId: String? = VoiceConfig.UserID) { - buildEvaluateConfig(originText, evalTargetType, userId) - //开始测评 - mSingEngine?.start() - mCurEvalType = evalTargetType - Log.w(TAG, "startRecord originText=$originText evalTargetType =$evalTargetType") - } - - /** - * 评测外部录音文件 - * @param voiceFilePath 录音文件路径 - */ - fun evaluate(voiceFilePath: String, originText: String, @EvalTargetType evalTargetType: Int? = EvalTargetType.SENTENCE, userId: String? = VoiceConfig.UserID) { - buildEvaluateConfig(originText, evalTargetType, userId) - if (mIsReady) { - mSingEngine?.startWithPCM(voiceFilePath) - Log.w(TAG, "startWithPCM") - } - mCurEvalType = evalTargetType - } - - private fun buildEvaluateConfig(originText: String, @EvalTargetType evalTargetType: Int? = EvalTargetType.SENTENCE, userId: String? = VoiceConfig.UserID) { - try { - val request = JSONObject() - when (evalTargetType) { - EvalTargetType.SENTENCE -> { - request.put("coreType", VoiceConfig.TYPE_SENT_KID) - .put("refText", originText.trim()) - .put("rank", 100) // 评分分制,这个值可以任意设置,最终会根据与 100 的比例重新计算 - .put("symbol", 1) // 用后标点符号 - .put("typeThres", SingSoundConfig.BASE_TYPETHRES) - .put("feedback", 1) // 是否开启实时评测 - } - EvalTargetType.ALPHA -> { - if (originText.trim() == "/ɔr/") { // ishow_unexpected1 = ɔr - val jsonObj = JSONObject() - jsonObj.put("abc", "ao r") - request.put("coreType", VoiceConfig.TYPE_WORD) - .put("refText", "abc") - // rateScale: 打分宽松度,0.8~1.5,默认 1.0。这个参数可以看作是个乘数,值越高打分越高。未 - //来版本可能会放弃支持此参数,不建议使用,可用 typeThres 参数代替。 - .put("typeThres", SingSoundConfig.BASE_TYPETHRES) - .put("precision", 1) // 评分精度,默认1 - .put("attachAudioUrl", 1) // 评分结果中是否包含音频 url - .put("phones", jsonObj) // 指定单词的发音。 - .put("rank", 100) - } else { - request.put("coreType", VoiceConfig.TYPE_ALPHA) - .put("typeThres", SingSoundConfig.BASE_TYPETHRES) - .put("refText", getSymbolText(originText.trim())) - .put("rank", 100) - } - } - EvalTargetType.WORD -> { - request.put("coreType", VoiceConfig.TYPE_WORD) - .put("refText", originText.trim()) - .put("typeThres", SingSoundConfig.BASE_TYPETHRES) - .put("typeThres", 0) - .put("phdet", 1) // 音素检错,1 表示使用此功能,默认为 0,不启动; 只能设置 0 和 1 - .put("syldet", 1) // 音节检错,1 表示使用此功能,默认为 0,不启动 只能设置 0 和 1 - // .put("syllable", 1) // (单词题型支持评测音节;可以设置 syllable 字段)评测音节信息,1 表示使用此功能,默认为 0,不启动;只能设置 0 和 1 - .put("rank", 100) - } - else -> { - request.put("coreType", VoiceConfig.TYPE_SENT_KID) - .put("refText", originText.trim()) - .put("typeThres", SingSoundConfig.BASE_TYPETHRES) - .put("rank", 100) // 评分分制,这个值可以任意设置,最终会根据与 100 的比例重新计算 - .put("symbol", 1) // 用后标点符号 - .put("feedback", false) // 是否开启实时评测 - } - } - - //构建评测请求参数 - val startCfg = mSingEngine?.buildStartJson(userId, request) - //设置评测请求参数 - mSingEngine?.setStartCfg(startCfg) - } catch (e: Exception) { - e.printStackTrace() - } - } - - fun stopRecord() { // 停止录音(有回调) - if (mIsReady) { - mSingEngine?.stop() - Log.w(TAG, "stopRecord") - } - } - - fun cancel() { // 取消录音(无回调onResult) - if (mIsReady) { - mCanceled = true - mSingEngine?.cancel() - mListeners?.let { - for (callback in it) { - callback.onCancel() - } - } - Log.w(TAG, "cancel") - } - } - - // 播放录音 - fun playBack() { -// if (mSingEngine != null) { -// val tokenid = SPUtils.getInstance().getString(VoiceConfig.cloud_sentece + 1) -// if (tokenid != null) { -// mSingEngine!!.playback() -// } -// } - if (mIsReady) { - mSingEngine?.playback() - Log.w(TAG, "playBack") - mCanceled = false - } - } - - /** - * 获取录音文件 - */ - fun getRecordFilePath(): String? { - return mSingEngine?.wavPath - } - - /** - * 停止播放录音 - */ - fun stopPlayBack() { - if (mIsReady) { - mSingEngine?.stopPlayBack() - Log.w(TAG, "stopPlayBack") - } - } - - // (录音播放无法暂停)中断并重新播放 - fun playWithInterrupt() { - if (mIsReady) { - mSingEngine?.playWithInterrupt() - Log.w(TAG, "playWithInterrupt") - } - } - - /** - * 停止录音、停止播放录音 - */ - fun release() { - if (mIsReady) { -// mSingEngine?.stopPlayBack() - mSingEngine?.deleteSafe() - } - mListeners?.clear() - mListeners = null - mCanceled = false - } - - override fun onAudioError(i: Int) { - Log.e(TAG, "onAudioError $i") - } - - /** - * 实时反馈回调 - * @param jsonObject - */ - override fun onRealTimeEval(jsonObject: JSONObject) { - Log.d(TAG, "onRealTimeEval = $jsonObject") - val realTimeResult = parseResult4Real(jsonObject) - if (realTimeResult?.realtime_details?.all { it.dp_type == 0 } == true) { - stopRecord() - } - } - - /** - * 录音开始回调(可以提示用户录音开始或者开始动画等逻辑) - */ - override fun onBegin() { - Log.i(TAG, "onBegin") - mListeners?.let { - for (callback in it) { - callback.onRecordBegin() - } - } - mCanceled = false - } - - /** - * 返回评测结果,评测结果为JSON格式 - */ - override fun onResult(jsonObject: JSONObject) { - Log.i(TAG, "onResult = $jsonObject") - setTokenToCache(jsonObject) - mListeners?.let { - for (callback in it) { - callback.onResult(toMap(jsonObject), mCurEvalType) - } - } - } - - private fun setTokenToCache(result: JSONObject) { - try { - if (result.has("tokenId")) { - val tokenID = result.getString("tokenId") - Log.e("tokenid", tokenID) - } - } catch (e: Exception) { - e.printStackTrace() - } - } - - /** - * 实时返回用户录音的音量大小 录音过程中会不断的回调此方法, - * 实时返回音量大小,volume取值范围为0\~100。 - * 用户可以根据volume的大小来实现用户录音音量大小的动画效果。 - */ - override fun onUpdateVolume(volume: Int) {} - - /** - * 开启录音后,一直没有声音输入,前置超时(检测到没有录音)会调用此方法, - * 用户可自己决定操作stop()或者cancel()。 - */ - override fun onFrontVadTimeOut() { - Log.i(TAG, "onFrontVadTimeOut") - stopRecord() - } - - /** - * 录音一段时间后不说话,后置超时,引擎自动调用stop(),结束录音并返回结果。 - * 用户可监听此方法用于更新录音的UI界面 - */ - override fun onBackVadTimeOut() { - Log.i(TAG, "onBackVadTimeOut") - } - - override fun onRecordingBuffer(bytes: ByteArray, i: Int) { - } - - /** - * 评测录音长度超时回调 - * 开发者可在该回调里调用stop()方法等待返回测评结果,或不做任何处理等待录音超时的错误码。 - * 通过超时错误码提示产品的用户。 - */ - override fun onRecordLengthOut() { - Log.i(TAG, "onRecordLengthOut") - stopRecord() - } - - override fun onReady() { - Log.i(TAG, "onReady") - mIsReady = true - mCanceled = false - } - - /** - * 播放录音完成回调 - */ - override fun onPlayCompeleted() { - Log.i(TAG, "onPlayCompeleted") - mListeners?.let { - for (callback in it) { - callback.onRecordPlayOver() - } - } - } - - /** - * 当录音停止并写入成功后回调 - */ - override fun onRecordStop() { - Log.i(TAG, "onRecordStop mCanceled = $mCanceled") - if (!mCanceled) { - mListeners?.let { - for (callback in it) { - callback.onRecordStop() - } - } - } else { - mCanceled = false - } - } - - /** - * 返回评测或初始化引擎失败原因,resultBody.getCode() 等于0为正确返回, - * 其他错误码见下错误码说明 - */ - override fun onEnd(resultBody: ResultBody) { - Log.i(TAG, "onEnd resultBody=$resultBody") - if (resultBody.code != 0) { - mListeners?.let { - for (callback in it) { - callback.onRecordFail(resultBody.code, resultBody.message) - } - } - } - } - - override fun onGetEvalRequestId(p0: String?) { - - } - - fun addOnResultListener(listener: SingEngineLifecycles) { - Log.i(TAG, "addOnResultListener") - if (mListeners?.contains(listener) == true) { - return - } - mListeners?.add(listener) - } - - fun removeOnResultListener(listener: SingEngineLifecycles) { - Log.i(TAG, "removeOnResultListener") - if (mListeners?.contains(listener) == true) { - mListeners?.remove(listener) - } - } - - /** - * 音标转化表 - */ - private fun getSymbolsMap() { - val linkedHashMap = - Gson().fromJson(WordConfig.getMapJSONObject().toString(), LinkedHashMap::class.java) - for (key in linkedHashMap.keys) { -// mAlphaMap[linkedHashMap[key].toString()] = key.toString() - mAlphaMap[key.toString()] = linkedHashMap[key].toString() - } - } - - /** - * 音标文本前后有"/",去掉 - */ - private fun getSymbolText(s: String): String? { - val substring = s.substring(1, s.length - 1) - return if (mAlphaMap.containsKey(substring)) { - mAlphaMap[substring] - } else "" - } -} - diff --git a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/bean/BaseResultModel.kt b/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/bean/BaseResultModel.kt deleted file mode 100644 index 2005dd6..0000000 --- a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/bean/BaseResultModel.kt +++ /dev/null @@ -1,83 +0,0 @@ -package com.kouyuxingqiu.wow_english.singsound.bean - -import android.os.Parcel -import android.os.Parcelable - -/** - * @author stay - * @date 2019-1-7 - * @describe 语音测评结果model - */ -class BaseResultModel( - var originText: String? = "", // 原始句子 - var score: Double = 0.0, // 总评分 - var scores: MutableList = mutableListOf(), - var pronounce: Double = 0.0, // 发音得分 - var fluency: Double = 0.0 // 流利度得分 -) : Parcelable { - - constructor(parcel: Parcel) : this( - parcel.readString(), - parcel.readDouble(), - mutableListOf().apply { - parcel.readTypedList(this, SingleResultModel.CREATOR) - }, - parcel.readDouble(), - parcel.readDouble() - ) { - } - - // 每个单词测评结果model - class SingleResultModel( - val char: String?, - val score: Double - ) : Parcelable { - constructor(parcel: Parcel) : this( - parcel.readString(), - parcel.readDouble() - ) { - } - - override fun writeToParcel(parcel: Parcel, flags: Int) { - parcel.writeString(char) - parcel.writeDouble(score) - } - - override fun describeContents(): Int { - return 0 - } - - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): SingleResultModel { - return SingleResultModel(parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } - } - - override fun writeToParcel(parcel: Parcel, flags: Int) { - parcel.writeString(originText) - parcel.writeDouble(score) - parcel.writeTypedList(scores) - parcel.writeDouble(pronounce) - parcel.writeDouble(fluency) - } - - override fun describeContents(): Int { - return 0 - } - - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): BaseResultModel { - return BaseResultModel(parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } -} - diff --git a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/bean/FullEvaluationResult.txt b/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/bean/FullEvaluationResult.txt deleted file mode 100644 index c1a7e51..0000000 --- a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/bean/FullEvaluationResult.txt +++ /dev/null @@ -1,133 +0,0 @@ -{ - "tokenId":"5fdcaf04332793000004f230", - "applicationId":"t418", - "audioUrl":"http://trial-files.api.cloud.ssapi.cn:8080/t418/11eb41353b71e226b8b2t418n29112b3", - "connect":{ - "param":{ - "app":{ - "timestamp":"1608298240", - "applicationId":"t418", - "sig":"684607db2bcbb3ee675681e0a43cb461a6540649" - }, - "sdk":{ - "os":"android", - "product":"fake", - "os_version":"0.0", - "source":1, - "protocol":1, - "type":1, - "arch":"armv8l", - "version":16779008 - } - }, - "cmd":"connect" - }, - "params":{ - "app":{ - "timestamp":"1608298244", - "userId":"guest", - "sig":"23591de4a3fbc5a568acf221cdd63769693f7053", - "connect_id":"5fdcaf003327930000038230", - "clientId":"", - "applicationId":"t418" - }, - "audio":{ - "saveAudio":0, - "sampleBytes":2, - "audioType":"ogg", - "sampleRate":16000, - "channel":1 - }, - "request":{ - "request_id":"5fdcaf04332793000005f230", - "tokenId":"5fdcaf04332793000004f230", - "coreType":"en.sent_kid.score", - "attachAudioUrl":1, - "typeThres":2, - "feedback":1, - "refText":"hello", - "symbol":1, - "rank":100 - } - }, - "recordId":"11eb41353b71e226b8b2t418n29112b3", - "refText":"hello", - "dtLastResponse":"2020-12-18 21:30:48:128", - "cloud_platform":{ - "origin_audio_length":10143 - }, - "result":{ - "overall":0, - "forceout":0, - "precision":1, - "systime":2758, - "res":"eng.snt_kid.online.1.0", - "rank":100, - "rhythm":{ - "stress":0, - "overall":50, - "tone":0, - "sense":100 - }, - "fluency":{ - "pause":0, - "overall":0, - "speed":0 - }, - "pron":0, - "wavetime":2610, - "accuracy":0, - "details":[ - { - "dp_type":1, - "tonescore":0, - "dur":0, - "liaisonref":0, - "stressref":0, - "senseref":1, - "start":0, - "liaisonscore":0, - "fluency":0, - "char":"hello", - "toneref":0, - "stressscore":0, - "score":0, - "end":0, - "sensescore":0 - } - ], - "info":{ - "tipId":10004, - "clip":0, - "snr":0, - "volume":51 - }, - "statics":[ - { - "score":0, - "char":"hh", - "count":1 - }, - { - "score":0, - "char":"eh", - "count":1 - }, - { - "score":0, - "char":"l", - "count":1 - }, - { - "score":0, - "char":"ow", - "count":1 - } - ], - "delaytime":107, - "integrity":0, - "pretime":1, - "version":"0.0.80.2020.11.18.19:18:30" - }, - "eof":1 -} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/bean/RealtimeResultEntity.kt b/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/bean/RealtimeResultEntity.kt deleted file mode 100644 index 83728cd..0000000 --- a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/bean/RealtimeResultEntity.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.kouyuxingqiu.wow_english.singsound.bean - -/** - * @author: stay - * @date: 2020/9/1 11:40 - * @description: - */ -data class RealtimeResultEntity( - val result_desc: String = "", - val eof: Int = -1, // 0 表示返回未结束,后续还有其它的返回结果 1:表示本次评测所有的返回结束 - val realtime_details: List? = mutableListOf(), - val result_type: Int = 0 -) - - -data class RealtimeDetailEntity( - val dp_type: Int = -1, // 0:表示正常读 1:表示漏读或者未读 2:表示重读 - val char: String = "", // 单词发音得分 - val start: Int = 0, // 单词在音频中的起始时间,单位为毫秒 (ms) - val end: Int = 0, // 单词在音频中的结束时间,单位为毫秒 (ms) - val dur: Int = 0, // 单词发音时间,单位为毫秒(ms) - val score: Int = 0 -) \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/bean/SentenceCode.java b/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/bean/SentenceCode.java deleted file mode 100644 index 3788dd4..0000000 --- a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/bean/SentenceCode.java +++ /dev/null @@ -1,112 +0,0 @@ -package com.kouyuxingqiu.wow_english.singsound.bean; - -import android.os.Parcel; -import android.os.Parcelable; - -/** - * 句子的详细信息 - * Created by wangz on 2017/8/30. - */ -public class SentenceCode implements Parcelable { - public String charStr; // 单词 - public int score; // 单词发音得分 - public int fakePron; // 词典中未找到单词对应的发音 - public int start; // 单词在音频中的起始时间,单位为毫秒(ms) - public int end; // 单词在音频中的结束时间,单位为毫秒(ms) - public int dur; // 单词发音时间 - public double fluency; // 流利度评分(0-100) - public int stressref; // 重读标识 - public int stressscore; // 重读得分(0、1) - public int toneref; // 升调标识 - public int tonescore; // 升降调得分(0、1) - public int senseref; // 意群停顿标识 - public int sensescore; // 意群停顿得分(0、1) - public int liaisonref; // 连读标识 - public int liaisonscore; // 连读得分(0、1) - public int dpType; // 单词正常朗读(不输出dp_type字段)、漏读(1)、重复读(2) - public int isPause; // 停顿标记 - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeString(this.charStr); - dest.writeInt(this.score); - dest.writeInt(this.fakePron); - dest.writeInt(this.start); - dest.writeInt(this.end); - dest.writeInt(this.dur); - dest.writeDouble(this.fluency); - dest.writeInt(this.stressref); - dest.writeInt(this.stressscore); - dest.writeInt(this.toneref); - dest.writeInt(this.tonescore); - dest.writeInt(this.senseref); - dest.writeInt(this.sensescore); - dest.writeInt(this.liaisonref); - dest.writeInt(this.liaisonscore); - dest.writeInt(this.dpType); - dest.writeInt(this.isPause); - } - - public SentenceCode() { - } - - protected SentenceCode(Parcel in) { - this.charStr = in.readString(); - this.score = in.readInt(); - this.fakePron = in.readInt(); - this.start = in.readInt(); - this.end = in.readInt(); - this.dur = in.readInt(); - this.fluency = in.readDouble(); - this.stressref = in.readInt(); - this.stressscore = in.readInt(); - this.toneref = in.readInt(); - this.tonescore = in.readInt(); - this.senseref = in.readInt(); - this.sensescore = in.readInt(); - this.liaisonref = in.readInt(); - this.liaisonscore = in.readInt(); - this.dpType = in.readInt(); - this.isPause = in.readInt(); - } - - public static final Creator CREATOR = new Creator() { - @Override - public SentenceCode createFromParcel(Parcel source) { - return new SentenceCode(source); - } - - @Override - public SentenceCode[] newArray(int size) { - return new SentenceCode[size]; - } - }; - - @Override - public String toString() { - return "SentenceCode{" + - "charStr='" + charStr + '\'' + - ", score=" + score + - ", fakePron=" + fakePron + - ", start=" + start + - ", end=" + end + - ", dur=" + dur + - ", fluency=" + fluency + - ", stressref=" + stressref + - ", stressscore=" + stressscore + - ", toneref=" + toneref + - ", tonescore=" + tonescore + - ", senseref=" + senseref + - ", sensescore=" + sensescore + - ", liaisonref=" + liaisonref + - ", liaisonscore=" + liaisonscore + - ", dpType=" + dpType + - ", isPause=" + isPause + - '}'; - } -} diff --git a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/bean/SentenceRealTimeEntity.java b/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/bean/SentenceRealTimeEntity.java deleted file mode 100644 index 4e632ed..0000000 --- a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/bean/SentenceRealTimeEntity.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.kouyuxingqiu.wow_english.singsound.bean; - -import android.os.Parcel; -import android.os.Parcelable; - -/** - * 实时评测句子的实体 - * Created by yxw on 2018/9/13 - */ -public class SentenceRealTimeEntity implements Parcelable { - public String charStr; - public int dp_type; - - public SentenceRealTimeEntity() { - - } - - - protected SentenceRealTimeEntity(Parcel in) { - charStr = in.readString(); - dp_type = in.readInt(); - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeString(charStr); - dest.writeInt(dp_type); - } - - @Override - public int describeContents() { - return 0; - } - - public static final Creator CREATOR = new Creator() { - @Override - public SentenceRealTimeEntity createFromParcel(Parcel in) { - return new SentenceRealTimeEntity(in); - } - - @Override - public SentenceRealTimeEntity[] newArray(int size) { - return new SentenceRealTimeEntity[size]; - } - }; -} diff --git a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/bean/SentenceResultEntity.java b/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/bean/SentenceResultEntity.java deleted file mode 100644 index e4b0461..0000000 --- a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/bean/SentenceResultEntity.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.kouyuxingqiu.wow_english.singsound.bean; - -/** - * 句子 - * Created by wangz on 2017/8/29. - */ -public class SentenceResultEntity { - public String oldSent; // 原始句子 - public String resultSent; // 返回的结果 - - // 返回的评测结果 - public double overall; // 单词的总分数 - - public double integrity; // 完整度 - public String missing = "无"; // 遗漏词汇 - public String repeat = "无"; // 复读词汇 - public String points; // 识别要点 - - public double accuracy; // 准确度 - public String continuity = "无"; // 连续现象 - public int intonation; // 句子语调 - public String errorWords; // 错词统计 - - public double fluency; // 流利度 - public double speed; // 平均语速 - public int pause; // 停顿过长 - - public int toneref; // 升降调标识 - public int tonescore; // 升降调得分(0、1) - -} diff --git a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/bean/StressCode.java b/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/bean/StressCode.java deleted file mode 100644 index c53fb2b..0000000 --- a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/bean/StressCode.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.kouyuxingqiu.wow_english.singsound.bean; - -import android.os.Parcel; -import android.os.Parcelable; - -/** - * Created by wangz on 2017/8/29. - */ - -public class StressCode implements Parcelable { - private String charText; // 重音 - private int ref; // 标识当前音节是否需要重读 - private int score; // 重音得分(0、1) - - public String getCharText() { - return charText; - } - - public void setCharText(String charText) { - this.charText = charText; - } - - public int getRef() { - return ref; - } - - public void setRef(int ref) { - this.ref = ref; - } - - public int getScore() { - return score; - } - - public void setScore(int score) { - this.score = score; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeString(this.charText); - dest.writeInt(this.ref); - dest.writeInt(this.score); - } - - public StressCode() { - } - - protected StressCode(Parcel in) { - this.charText = in.readString(); - this.ref = in.readInt(); - this.score = in.readInt(); - } - - public static final Creator CREATOR = new Creator() { - @Override - public StressCode createFromParcel(Parcel source) { - return new StressCode(source); - } - - @Override - public StressCode[] newArray(int size) { - return new StressCode[size]; - } - }; - - @Override - public String toString() { - return "StressCode{" + - "charText='" + charText + '\'' + - ", ref=" + ref + - ", score=" + score + - '}'; - } -} diff --git a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/bean/WordCode.java b/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/bean/WordCode.java deleted file mode 100644 index 6dd6fcd..0000000 --- a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/bean/WordCode.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.kouyuxingqiu.wow_english.singsound.bean; - -/** - * Created by wang on 2016/12/8. - */ -public class WordCode { - - private String charText; // 单个的单词 - private double score; // 单个单词的得分 - private double refScore = -999; // 重音的单词得分 - - public String getCharText() { - return charText; - } - - public void setCharText(String charText) { - this.charText = charText; - } - - public double getScore() { - return score; - } - - public void setScore(double score) { - this.score = score; - } - - public double getRefScore() { - return refScore; - } - - public void setRefScore(double refScore) { - this.refScore = refScore; - } -} diff --git a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/config/EvalTargetType.java b/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/config/EvalTargetType.java deleted file mode 100644 index 665a62a..0000000 --- a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/config/EvalTargetType.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.kouyuxingqiu.wow_english.singsound.config; - -import androidx.annotation.IntDef; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -/** - * @author stay - * @date 2019-2-1 - * @describe 语音评测类型 - */ -@Retention(RetentionPolicy.SOURCE) -@IntDef({EvalTargetType.SENTENCE, EvalTargetType.ALPHA, EvalTargetType.WORD}) -public @interface EvalTargetType { - int SENTENCE = 1; // 句子 - int ALPHA = 2; // 音标 - int WORD = 3; // 单词 -} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/config/SentenceConfig.java b/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/config/SentenceConfig.java deleted file mode 100644 index 6470858..0000000 --- a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/config/SentenceConfig.java +++ /dev/null @@ -1,126 +0,0 @@ -package com.kouyuxingqiu.wow_english.singsound.config; - -import com.kouyuxingqiu.wow_english.singsound.bean.SentenceResultEntity; -import com.kouyuxingqiu.wow_english.singsound.util.JsonUtils; - -import org.json.JSONArray; -import org.json.JSONObject; - - -/** - * @author stay - * @date 2019-1-7 - * @describe - */ -public class SentenceConfig { - - /** - * 解析得到英文句子的一些信息 - * - * @param result - * @return SentenceResultEntity - */ - public static SentenceResultEntity resultJson(JSONObject result) { - SentenceResultEntity sentenceEntity = new SentenceResultEntity(); - double overall = 0; - double integrity = 0; - double accuracy = 0; - double fluencyOverall = 0; - int fluencyPause = 0; - double fluencySpeed = 0; - int intonation = 0; - int toneref = -999; - int tonescore = -999; // 升降调 - - String missingStr = ""; // 漏读 - String repeatStr = ""; // 复读 - int pauseStr = 0; - String continuity = ""; - - try { - if (result.has("result")) { - JSONObject resultJ = JsonUtils.getJsonObject(result, "result"); - overall = JsonUtils.getDouble(resultJ, "overall"); - // 完整度 - integrity = JsonUtils.getDouble(resultJ, "integrity"); - // 准确度 - accuracy = JsonUtils.getDouble(resultJ, "accuracy"); - // 流利度 - JSONObject fluency = JsonUtils.getJsonObject(resultJ, "fluency"); - fluencyOverall = JsonUtils.getInt(fluency, "overall"); // 总分 - fluencyPause = JsonUtils.getInt(fluency, "pause"); // 停顿次数 - fluencySpeed = JsonUtils.getInt(fluency, "speed"); // 0:慢,1:正常,2:快 - - // 遍历所有词汇 - JSONArray detailsA = JsonUtils.getJsonArray(resultJ, "details"); - boolean isLiaisonscore = false; // 下一个单词是否连续 - int missingIndex = 0; - int repeatIndex = 0; - for (int i = 0; i < detailsA.length(); i++) { - JSONObject detailsWords = detailsA.getJSONObject(i); - - String charStr = JsonUtils.getString(detailsWords, "char"); - int dpType = JsonUtils.getInt(detailsWords, "dp_type"); // 漏读的才会有 - - // 过滤掉多余符号 - charStr = charStr.replace(".", ""); - charStr = charStr.replace(",", ""); - - // TODO 漏读与重复读 - if (dpType == 1 && missingIndex < 3) { // 漏读 - missingStr += charStr + (detailsA.length() - 1 == i ? "" : ", "); -// missingIndex++; - } else if (dpType == 2 && repeatIndex < 3) { // 重复读 - repeatStr += charStr + (detailsA.length() - 1 == i ? "" : ", "); -// repeatIndex++; - } - -// if (missingIndex == 3) { -// missingStr += "..."; -// missingIndex++; -// } -// if (repeatIndex == 3) { -// repeatStr += "..."; -// repeatIndex++; -// } - - pauseStr += JsonUtils.getInt(detailsWords, "is_pause"); - - int liaisonscore = JsonUtils.getInt(detailsWords, "liaisonscore"); - if (isLiaisonscore) { - continuity += charStr + (detailsA.length() - 1 == i ? "" : ", "); - isLiaisonscore = false; - } - - if (liaisonscore == 1) { - continuity += charStr + " "; - isLiaisonscore = true; - } - - // TODO 添加升降调 - toneref = JsonUtils.getInt(detailsWords, "toneref"); - tonescore = JsonUtils.getInt(detailsWords, "tonescore"); - if (detailsA.length() - 1 == i) { - tonescore = tonescore == toneref ? 90 : 10; - } - } - } - // 更新 - sentenceEntity.overall = overall; - sentenceEntity.integrity = integrity; - sentenceEntity.missing = "".equals(missingStr) ? "无" : missingStr; - sentenceEntity.repeat = "".equals(repeatStr) ? "无" : repeatStr; - sentenceEntity.accuracy = accuracy; - sentenceEntity.intonation = tonescore; - sentenceEntity.fluency = fluencyOverall; - sentenceEntity.speed = fluencySpeed; - sentenceEntity.pause = fluencyPause; - sentenceEntity.continuity = "".equals(continuity) ? "无" : continuity; - sentenceEntity.toneref = toneref; - sentenceEntity.tonescore = tonescore; - } catch (Exception e) { - e.printStackTrace(); - } - return sentenceEntity; - } -} diff --git a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/config/SingSoundConfig.java b/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/config/SingSoundConfig.java deleted file mode 100644 index c24a36f..0000000 --- a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/config/SingSoundConfig.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.kouyuxingqiu.wow_english.singsound.config; - -/** - * 页面描述: 先声配置类 - * create by yxw on 2018/12/20 - */ -public class SingSoundConfig { - public static final String APPKEY_DEBUG = "t418"; - public static final String SECERTKEY_DEBUG = "1a16f31f2611bf32fb7b3fc38f5b2c81"; - - public static final String APPKEY = "a418"; - public static final String SECERTKEY = "c11163aa6c834a028da4a4b30955be99"; - - public static final float BASE_TYPETHRES = 2f; // 打分宽松度 -} diff --git a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/config/VoiceConfig.java b/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/config/VoiceConfig.java deleted file mode 100644 index a9ddfc2..0000000 --- a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/config/VoiceConfig.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.kouyuxingqiu.wow_english.singsound.config; - -/** - * Created by wang on 2016/8/25. - */ -public class VoiceConfig { - - // 英文段落朗读 - public static final String TYPE_Paragraph = "en.pred.score"; - public static final String TYPE_WORD = "en.word.score"; - public static final String TYPE_ALPHA = "en.alpha.score"; - public static final String TYPE_SENT = "en.sent.score"; - public static final String TYPE_WORD_KID = "en.word_kid.score"; - public static final String TYPE_SENT_KID = "en.sent_kid.score"; - public static final String TYPE_CN_WORD = "cn.word.score"; - public static final String TYPE_CN_SENT = "cn.sent.score"; - public static final String TYPE_PCHA = "en.pcha.score"; - public static final String TYPE_choc = "en.choc.score"; - public static final String TYPE_Question_answer = "en.pqan.score"; - public static final String TYPE_pic_article = "en.pict.score"; - - public static final String UserID = "guest"; - - public static final String QVA_PARA = " Where can I buy some medicine? I get seasick. Go to Lang's Drugstore. How do I get to Lang's Drugstore? Do you have a map? No. OK. I can draw it. Go two blocks and turn left. Pardon? Do I turn left at the bank? No, don't turn left at the bank. Turn left at the post office. At the post office? Yes. There. Turn left there. Then what? Go through the park and around the circle. Then go straight on Main Street to the coffee shop. How far is that? About two miles. Don't go more than two miles. And then am I there? No, there's a big sign: Lang's Drugstore. Turn right at the sign. Then you're there. Park your car and go up the steps. OK. Thanks for the directions, Rita. Yes, I often walk around my city. You see, I often go shopping and in my free time I would like to stay with my friends at the coffee shop. Generally, I am familiar with the roads around my house. I often walk back home along those roads after work, and I can remember their names quite well. Yes, I can. I am ready to help others. I think helping others will make me feel happy and useful. So I would answer others’ questions with patience. How many blocks should the man go before he turns left? Where should the man turn left? What does he often do in his free time? How does he go home after work? What does he think of helping others? "; - - public static final String[] paragraphs = { - "It is very good for our health to do some sports. Basketball is my favourite sport. At weekends, I often play basketball with my friends in the park. I think it is the best way to release my pressure. Also, I can feel more enjoyable from playing basketball. My favourite basketball star is Yaoming. I am going to practise playing basketball everyday so that I can become a basketball player when I grow up.", - "It is dawn and the sun is rising, as it has every day for the last 5 billion years. It has been a constant golden disk, shining its unchanging light onto the Earth. But look through the glare, and the true face of the sun is revealed. Not constant, but constantly changing. To understand the sun is to understand the forces that drive the universe. If we can control those forces, we can unlock the power of the stars. All life on Earth owes its existence to the sun. It powers every natural system and sustains every plant and animal."}; - - - public static final String native_word = "native_word"; - public static final String native_sentece = "native_sentece"; - public static final String cloud_word = "cloud_word"; - public static final String cloud_acom_sentece = "cloud_acom_sentece";//音频对比 - public static final String cloud_sentece = "cloud_sentece"; - public static final String cloud_cn_word = "cloud_cn_word"; - public static final String cloud_cn_sentece = "cloud_cn_sentece"; - public static final String cloud_para = "cloud_para"; - public static final String cloud_choic = "cloud_choic"; - public static final String cloud_quest = "cloud_quest"; - public static final String cloud_article = "cloud_article"; - - -} diff --git a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/config/WordConfig.java b/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/config/WordConfig.java deleted file mode 100644 index f5d45f0..0000000 --- a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/config/WordConfig.java +++ /dev/null @@ -1,537 +0,0 @@ -package com.kouyuxingqiu.wow_english.singsound.config; - -import android.util.Log; - -import com.google.gson.Gson; -import com.kouyuxingqiu.wow_english.singsound.bean.SentenceCode; -import com.kouyuxingqiu.wow_english.singsound.bean.SentenceRealTimeEntity; -import com.kouyuxingqiu.wow_english.singsound.bean.StressCode; -import com.kouyuxingqiu.wow_english.singsound.bean.WordCode; -import com.kouyuxingqiu.wow_english.singsound.util.JsonUtils; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; - -/** - * Created by wang on 2016/12/8. - */ - -public class WordConfig { - - /** - * 单词:获取音素 - */ - public static List getWordsPhoneList(JSONObject json) { - List list = new ArrayList<>(); - JSONObject map_json = getMapJSONObject(); - - if (map_json != null) { - // 数据正确 - if (json.has("result")) { - JSONObject json_result = JsonUtils.getJsonObject(json, "result"); - JSONArray json_details = JsonUtils.getJsonArray(json_result, "details"); - JSONObject jsonObject = JsonUtils.getJsonObject(json_details, 0); - - if (jsonObject != null) { - // 获得重音发音 - if (jsonObject.has("phone")) { - JSONArray phoneJA = JsonUtils.getJsonArray(jsonObject, "phone"); - - // 前/ - WordCode fontItem = new WordCode(); - fontItem.setCharText("/ "); - fontItem.setScore(-1); - list.add(fontItem); - - // 解析中间的音素 - for (int i = 0; i < phoneJA.length(); i++) { - // ------------------------ 解析音素相关逻辑 ----------------------------- - JSONObject wordJB = JsonUtils.getJsonObject(phoneJA, i); - WordCode wordCodeItem = new WordCode(); - String text = JsonUtils.getString(wordJB, "char"); - String newText = JsonUtils.getString(map_json, text); - if (newText != null) { - text = newText; - } - - wordCodeItem.setCharText(text); - wordCodeItem.setScore(JsonUtils.getDouble(wordJB, "score")); - list.add(wordCodeItem); - } - - // 后/ - WordCode backItem = new WordCode(); - backItem.setCharText(" /"); - backItem.setScore(-1); - list.add(backItem); - - // ------------------------ 解析重音相关逻辑 ----------------------------- - List stressCodesList = getWordsStressList(json); - for (int i = 0; i < stressCodesList.size(); i++) { - StressCode stressCode = stressCodesList.get(i); - WordCode wordCode = list.get(i); - if (!wordCode.getCharText().equals(stressCode.getCharText())) { - WordCode moreWordCode = new WordCode(); - moreWordCode.setCharText(stressCode.getCharText()); - moreWordCode.setScore(stressCode.getScore() == 1 ? 90 : 10); - - // 显示黑色 - if (" · ".equals(stressCode.getCharText())) { - moreWordCode.setScore(70); - } - list.add(i, moreWordCode); - } - // TODO good 写死 - if ("g".equals(wordCode.getCharText())) { - WordCode c = list.get(i - 1); - c.setScore(70); - } - } - } - } - } - } - return list; - } - - /** - * 单词:获取重音 - */ - public static List getWordsStressList(JSONObject json) { - List list = new ArrayList<>(); - JSONObject map_json = getMapJSONObject(); - - try { - if (json != null) { - // 数据正确 - if (json.has("result")) { - JSONObject json_result = json.getJSONObject("result"); - if (json_result.has("details")) { - JSONArray json_details = json_result.getJSONArray("details"); - JSONObject jsonObject = (JSONObject) json_details.get(0); - - if (jsonObject != null) { - // 获得重音发音 - if (jsonObject.has("stress")) { - JSONArray json_phone = jsonObject.getJSONArray("stress"); - if (json_phone != null) { - //add 前/ - StressCode fontItem = new StressCode(); - fontItem.setCharText("/ "); - fontItem.setScore(-1); - list.add(fontItem); - - // 解析重音相关 - for (int i = 0; i < json_phone.length(); i++) { - JSONObject json_bean = (JSONObject) json_phone.get(i); - - int ref = json_bean.getInt("ref"); - - // 判断是不是重音 - if (ref == 1) { // 1 是重读 - StressCode stressItem = new StressCode(); - stressItem.setCharText("'"); - stressItem.setScore(json_bean.getInt("score")); - list.add(stressItem); - } - - String text = json_bean.getString("char"); - String[] texts = text.split("_"); - - for (String text1 : texts) { - StressCode wordCodeItem = new StressCode(); - wordCodeItem.setScore(-1); - wordCodeItem.setRef(ref); - wordCodeItem.setCharText((String) map_json.get(text1)); - - // TODO 直接写死 如果是 good 前面添加一个重音 - if ("g".equals(map_json.get(text1))) { - StressCode stressItem = new StressCode(); - stressItem.setCharText("'"); - stressItem.setScore(70); - list.add(stressItem); - } - - list.add(wordCodeItem); - } - - if (i + 1 != json_phone.length()) { - StressCode pointItem = new StressCode(); - pointItem.setCharText(" · "); - pointItem.setScore(-1); - list.add(pointItem); - } - } - - //add 后/ - StressCode backItem = new StressCode(); - backItem.setCharText(" /"); - backItem.setScore(-1); - list.add(backItem); - } - } - } - } - } - } - } catch (JSONException e) { - e.printStackTrace(); - } - return list; - } - - public static List getSentenceRealTimeList(JSONObject json) { - List list = new ArrayList<>(); - if (json != null) { - if (json.has("result")) { - JSONObject resultJB = JsonUtils.getJsonObject(json, "result"); - if (resultJB.has("realtime_details")) { - JSONArray jsonDetails = JsonUtils.getJsonArray(resultJB, "realtime_details"); - int length = jsonDetails.length(); - for (int i = 0; i < length; i++) { - JSONObject itemSentence = JsonUtils.getJsonObject(jsonDetails, i); - SentenceRealTimeEntity sentenceRealTime = new SentenceRealTimeEntity(); - sentenceRealTime.charStr = JsonUtils.getString(itemSentence, "char") + " "; - sentenceRealTime.dp_type = JsonUtils.getInt(itemSentence, "dp_type"); - list.add(sentenceRealTime); - } - } - } - } - return list; - - } - - public static List getCnSentenceList(JSONObject json) { - return getSentenceList(json, "chn_char"); - } - - public static List getEnSentenceList(JSONObject json) { - return getSentenceList(json, "char"); - } - - /** - * 获得句子的高亮 - * 每个单词的评分,拥有升降调与停顿的 - * - * @return - */ - private static List getSentenceList(JSONObject json, String type) { - List list = new ArrayList<>(); - if (json != null) { - // 数据正确 - if (json.has("result")) { - JSONObject resultJB = JsonUtils.getJsonObject(json, "result"); - if (resultJB.has("details")) { - JSONArray jsonDetails = JsonUtils.getJsonArray(resultJB, "details"); - - for (int i = 0; i < jsonDetails.length(); i++) { - JSONObject itemSentence = JsonUtils.getJsonObject(jsonDetails, i); - // 解析具体数据 - SentenceCode sentenceCode = new SentenceCode(); - sentenceCode.charStr = JsonUtils.getString(itemSentence, type) + " "; - sentenceCode.score = JsonUtils.getInt(itemSentence, "score"); - // 重复 - int dpType = JsonUtils.getInt(itemSentence, "dp_type"); - list.add(sentenceCode); - sentenceCode.score = dpType == 2 ? 120 : sentenceCode.score; // 120 显示黄色 - - // TODO 停顿 - sentenceCode.isPause = JsonUtils.getInt(itemSentence, "is_pause"); - if (sentenceCode.isPause == 1) { - // 句子停顿了,在后面的添加三个省略号 - SentenceCode pauseCode = new SentenceCode(); - pauseCode.charStr = "... "; - pauseCode.score = 10; - list.add(pauseCode); - } - - // TODO 添加升降调 - sentenceCode.toneref = JsonUtils.getInt(itemSentence, "toneref"); - sentenceCode.tonescore = JsonUtils.getInt(itemSentence, "tonescore"); - if (jsonDetails.length() - 1 == i) { - // 句子停顿了,在后面的添加三个省略号 - SentenceCode tonescoreCode = new SentenceCode(); - tonescoreCode.charStr = sentenceCode.toneref == 1 ? " ↗ " : " ↘ "; - tonescoreCode.score = sentenceCode.tonescore == sentenceCode.toneref ? 90 : 10; - list.add(tonescoreCode); - } - - // TODO 获得连续 - sentenceCode.liaisonscore = JsonUtils.getInt(itemSentence, "liaisonscore"); - } - } - } - } - return list; - } - - /** - * 获得句子的高亮 - * 每个单词的评分 - */ - public static List getSentenceBaseList(JSONObject json) { - List list = new ArrayList<>(); - if (json != null) { - // 数据正确 - if (json.has("result")) { - JSONObject resultJB = JsonUtils.getJsonObject(json, "result"); - if (resultJB.has("details")) { - JSONArray jsonDetails = JsonUtils.getJsonArray(resultJB, "details"); - for (int i = 0; i < jsonDetails.length(); i++) { - JSONObject itemOb = JsonUtils.getJsonObject(jsonDetails, i); - JSONArray itemA = JsonUtils.getJsonArray(itemOb, "snt_details"); - for (int j = 0; j < itemA.length(); j++) { - JSONObject detailsWords = JsonUtils.getJsonObject(itemA, j); - // 解析具体数据 - SentenceCode sentenceCode = new SentenceCode(); - sentenceCode.charStr = JsonUtils.getString(detailsWords, "char") + " "; - sentenceCode.score = JsonUtils.getInt(detailsWords, "score"); - list.add(sentenceCode); - - // TODO 停顿 - sentenceCode.isPause = JsonUtils.getInt(detailsWords, "is_pause"); - if (sentenceCode.isPause == 1) { - // 句子停顿了,在后面的添加三个省略号 - SentenceCode pauseCode = new SentenceCode(); - pauseCode.charStr = "... "; - pauseCode.score = 10; - list.add(pauseCode); - } - } - } - } - } - } - Log.w("ffffff", "itemA: " + list); - return list; - } - - private static List getStressWordCodeList(JSONArray jsonArray_stress, JSONArray jsonArray_phone) throws JSONException { - List stressList = getStressList(jsonArray_stress); - List phoneList = getPhoneList(jsonArray_phone); - JSONObject map_json = getMapJSONObject(); - - //把 phoneList 的分数 赋给 stressList - int m = 0; - for (int i = 0; i < stressList.size(); i++) { - for (int j = m; j < phoneList.size(); j++) { - WordCode stressItem = stressList.get(i); - if (stressItem.getCharText().equals(phoneList.get(j).getCharText())) { - stressItem.setScore(phoneList.get(j).getScore()); - m = j; - break; - } - } - } - - - for (int i = 0; i < stressList.size(); i++) { - Log.e("-----stressList-----", i + "-------" + stressList.get(i).getCharText() + "-------" + stressList.get(i).getScore()); - } - - for (int i = 0; i < phoneList.size(); i++) { - Log.d("-----phoneList-----", i + "-------" + phoneList.get(i).getCharText() + "-------" + phoneList.get(i).getScore()); - } - - - for (int i = 0; i < stressList.size(); i++) { - String stressText = stressList.get(i).getCharText(); - if (map_json.has(stressText)) { - String newstressText = map_json.getString(stressText); - if (newstressText != null) { - stressList.get(i).setCharText(newstressText); - } - } - } - - return stressList; - - } - - private static List getStressList(JSONArray jsonArray_stress) throws JSONException { - List stressList = new ArrayList<>(); - - //add 前/ - WordCode fontItem = new WordCode(); - fontItem.setCharText("/ "); - fontItem.setScore(-1); - stressList.add(fontItem); - - for (int i = 0; i < jsonArray_stress.length(); i++) { - JSONObject object = jsonArray_stress.getJSONObject(i); - - if (object.has("ref") && object.getInt("ref") == 1) { - WordCode fontStressItem = new WordCode(); - fontStressItem.setCharText("'"); - - if (object.getInt("score") == 1) { - fontStressItem.setScore(90); - } else { - fontStressItem.setScore(10); - } - stressList.add(fontStressItem); - } - - String charX = object.getString("char"); - String[] charXs = charX.split("_"); - - for (int j = 0; j < charXs.length; j++) { - WordCode stressItem = new WordCode(); - stressItem.setCharText(charXs[j]); - stressItem.setScore(0); - stressList.add(stressItem); - } - } - - //add 后/ - WordCode backItem = new WordCode(); - backItem.setCharText(" /"); - backItem.setScore(-1); - stressList.add(backItem); - - return stressList; - } - - - private static List getPhoneList(JSONArray json_phone) throws JSONException { - List list = new ArrayList<>(); - - - if (json_phone != null) { - - for (int i = 0; i < json_phone.length(); i++) { - JSONObject json_bean = (JSONObject) json_phone.get(i); - WordCode wordCodeItem = new WordCode(); - String text = json_bean.getString("char"); - wordCodeItem.setCharText(text); - wordCodeItem.setScore(json_bean.getDouble("score")); - list.add(wordCodeItem); - } - -// -// //add 后/ -// WordCode backItem = new WordCode(); -// backItem.setCharText(" /"); -// backItem.setScore(-1); -// list.add(backItem); - - } - - return list; - } - - - public static JSONObject getMapJSONObject() { - - LinkedHashMap map = new LinkedHashMap(); - try { - map.put("ɪ", "ih"); - map.put("I", "ih"); // 15 - map.put("ә", "ax"); - map.put("ə", "ax"); // 12 -// map.put("ɒ", "oo"); -// map.put("ɔ", "oo"); - map.put("ɒ", "aa"); - map.put("ɑ", "aa"); // 5 - map.put("ʊ", "uh"); - map.put("U", "uh"); // 14 - - map.put("ʌ", "ah"); - map.put("∧", "ah"); // 13 - map.put("e", "eh"); - map.put("ɛ", "eh"); // 4 - map.put("æ", "ae"); - map.put("i:", "iy"); - map.put("i", "iy"); // 8 - map.put("ɜ:", "er"); - - map.put("ɝ:", "axr"); - map.put("ɝ", "axr"); // 10 - map.put("ɚ", "axr"); // 10 - map.put("ɔ:", "ao"); - map.put("ɔ", "ao"); // 1 -//* map.put("ɔr", "ao r"); -// map.put("ɔr", "ao"); - map.put("u:", "uw"); - map.put("u", "uw"); // 9 - map.put("ju:", "y uw"); - - map.put("ɑr", "aa r"); - map.put("eɪ", "ey"); - map.put("aɪ", "ay"); - map.put("ɔɪ", "oy"); - map.put("aʊ", "aw"); - map.put("au", "aw"); // 11 - - map.put("әʊ", "ow"); - map.put("o", "ow"); // 2 -// map.put("ɪə", "ir"); -// map.put("ɪə", "ih r"); -//* map.put("ɪr", "ih r"); // 3 - map.put("ɪr", "ir"); // 3 -// map.put("eə", "ar"); -// map.put("eə", "eh r"); -//* map.put("ɛr", "eh r"); // 6 - map.put("ɛr", "ar"); // 6 - -// map.put("ʊə", "ur"); - map.put("ur", "ur"); - map.put("ʊə", "uh r"); - map.put("ʊr", "uh r"); // 7 - - - map.put("p", "p"); - map.put("k", "k"); - map.put("m", "mb"); - map.put("s", "s"); - map.put("f", "f"); - map.put("ʃ", "sh"); - map.put("ts", "ts"); - - map.put("b", "b"); - map.put("g", "g"); - map.put("n", "nb"); - map.put("z", "z"); - map.put("v", "v"); - map.put("ʒ", "zh"); - map.put("dz", "dz"); - - map.put("t", "t"); - map.put("l", "l"); - map.put("ŋ", "ng"); - map.put("θ", "th"); - map.put("w", "w"); - map.put("tʃ", "ch"); - map.put("tr", "tr"); - - map.put("d", "d"); - map.put("r", "r"); - map.put("h", "hh"); - map.put("ð", "dh"); - map.put("j", "y"); - map.put("dʒ", "jh"); - map.put("dr", "dr"); - - - Gson gson = new Gson(); - String s = gson.toJson(map); - - JSONObject json = new JSONObject(s); - - return json; - - } catch (JSONException e) { - e.printStackTrace(); - return null; - } - - } - - -} diff --git a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/util/JsonUtils.java b/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/util/JsonUtils.java deleted file mode 100644 index 6de7b14..0000000 --- a/android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/singsound/util/JsonUtils.java +++ /dev/null @@ -1,136 +0,0 @@ -package com.kouyuxingqiu.wow_english.singsound.util; - -import android.util.Log; - -import com.google.gson.Gson; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * Created by wangz on 2017/8/29. - */ - -public class JsonUtils { - private static final String TAG = "JSONUtils"; - - public static JSONObject getJsonObject(JSONArray array, int postion) { - try { - return array.getJSONObject(postion); - } catch (JSONException e) { - e.printStackTrace(); - return new JSONObject(); - } - } - - public static JSONObject getJsonObject(JSONObject object, String key) { - try { - return object.getJSONObject(key); - } catch (Exception e) { - Log.e(TAG, e.getLocalizedMessage()); - return new JSONObject(); - } - } - - public static JSONArray getJsonArray(JSONArray object, int key) { - try { - return object.getJSONArray(key); - } catch (Exception e) { - Log.e(TAG, e.getLocalizedMessage()); - return new JSONArray(); - } - } - - public static JSONArray getJsonArray(JSONObject object, String key) { - try { - return object.getJSONArray(key); - } catch (Exception e) { - Log.e(TAG, e.getLocalizedMessage()); - return new JSONArray(); - } - } - - public static String getString(JSONObject object, String key) { - try { - return object.getString(key); - } catch (Exception e) { - Log.e(TAG, e.getLocalizedMessage()); - return ""; - } - } - - public static int getInt(JSONObject object, String key) { - try { - return object.getInt(key); - } catch (Exception e) { - Log.e(TAG, e.getLocalizedMessage()); - return 0; - } - } - - public static double getDouble(JSONObject object, String key) { - try { - return object.getDouble(key); - } catch (Exception e) { - Log.e(TAG, e.getLocalizedMessage()); - return 0; - } - } - - public static boolean getBoolean(JSONObject object, String key) { - try { - return object.getBoolean(key); - } catch (Exception e) { - Log.e(TAG, e.getLocalizedMessage()); - return false; - } - } - - public static Map toMap(JSONObject jsonObject) throws JSONException { - Map map = new HashMap<>(); - Iterator keysIterator = jsonObject.keys(); - while (keysIterator.hasNext()) { - String key = keysIterator.next(); - Object value = jsonObject.get(key); - if (value instanceof JSONObject) { - value = toMap((JSONObject) value); - } - if (value instanceof JSONArray) { - value = toList((JSONArray) value); - } - map.put(key, value); - } - return map; - } - - public static List toList(JSONArray jsonArray) throws JSONException { - List list = new ArrayList<>(); - for (int i = 0; i < jsonArray.length(); i++) { - Object value = jsonArray.get(i); - if (value instanceof JSONObject || value instanceof JSONArray) { - value = toObject(value); - } - list.add(value); - } - return list; - } - - public static Object toObject(Object json) throws JSONException { - if (json instanceof JSONObject) { - return toMap((JSONObject) json); - } else if (json instanceof JSONArray) { - return toList((JSONArray) json); - } - return json; - } -} diff --git a/android/build.gradle b/android/build.gradle index f2c94fc..615fa7d 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -26,7 +26,6 @@ allprojects { // maven { url 'https://maven.aliyun.com/nexus/content/groups/public' } google() mavenCentral() - maven { url 'https://repo.singsound.com/repository/singsound_ginger_android_sdk/' } maven { url 'https://maven.zjzxsl.com/repository/android-public/' } } } diff --git a/assets/chivox/aiengine.provision b/assets/chivox/aiengine.provision new file mode 100644 index 0000000..806c1bd --- /dev/null +++ b/assets/chivox/aiengine.provision @@ -0,0 +1 @@ +ƙʚϙ̚̚ϝΚ˞ʙƜ˙Ξɞȝϙ̛ \ No newline at end of file diff --git a/assets/chivox/vad.0.13.bin b/assets/chivox/vad.0.13.bin new file mode 100644 index 0000000..c86ec1d --- /dev/null +++ b/assets/chivox/vad.0.13.bin diff --git a/ios/Podfile b/ios/Podfile index 06740b7..5ff47bc 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,4 @@ -source 'https://github.com/CocoaPods/Specs.git' -source 'https://pt.singsound.com:10081/singsound-public/SingSoundSDKCocoaPodRepo.git' +source 'https://cdn.cocoapods.org/' platform :ios, '12.0' @@ -32,7 +31,6 @@ flutter_ios_podfile_setup target 'Runner' do use_frameworks! use_modular_headers! - pod 'SingSoundSDK' pod 'DMProgressHUD' flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 775bc01..ce7d55b 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -10,8 +10,6 @@ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 52450AF12A4C415B007B3E4B /* XSMessageMehtodChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52450AF02A4C415B007B3E4B /* XSMessageMehtodChannel.swift */; }; - 525E171A2A4BD03900104CDF /* VoiceXSMessageChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 525E17192A4BD03900104CDF /* VoiceXSMessageChannel.swift */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; @@ -450,9 +448,7 @@ 3563EC8D55A646823FD26A83 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 48BCA0827DCB98991774F5AC /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; - 52450AF02A4C415B007B3E4B /* XSMessageMehtodChannel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSMessageMehtodChannel.swift; sourceTree = ""; }; 52450AF22A4ED0EC007B3E4B /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; - 525E17192A4BD03900104CDF /* VoiceXSMessageChannel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceXSMessageChannel.swift; sourceTree = ""; }; 6DEBBC1D861BE053F3ECE0B9 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -1027,8 +1023,6 @@ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, - 52450AF02A4C415B007B3E4B /* XSMessageMehtodChannel.swift */, - 525E17192A4BD03900104CDF /* VoiceXSMessageChannel.swift */, B852C1342BCABB5E00A53FC4 /* GameMessageChannel.swift */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, ); @@ -1955,7 +1949,6 @@ B891A85B2BD24EFB006CB06E /* AniSimpleButton.cpp in Sources */, B891A8282BD24EFB006CB06E /* TwoStateButton.cpp in Sources */, B891A8B42BD24EFB006CB06E /* ToyTouchableSprite.cpp in Sources */, - 525E171A2A4BD03900104CDF /* VoiceXSMessageChannel.swift in Sources */, B891A8842BD24EFB006CB06E /* ToyDragAndDropHandler.cpp in Sources */, B891A88C2BD24EFB006CB06E /* ToyLayoutObject.cpp in Sources */, B891A81D2BD24EFB006CB06E /* SimpleLevelPickerView.cpp in Sources */, @@ -2024,7 +2017,6 @@ B891A8102BD24EFB006CB06E /* ParentalGateShowInterface.cpp in Sources */, B891A82F2BD24EFB006CB06E /* AniBasicSteveMapCharacterController.cpp in Sources */, B891A80A2BD24EFA006CB06E /* LayoutParser.cpp in Sources */, - 52450AF12A4C415B007B3E4B /* XSMessageMehtodChannel.swift in Sources */, B891A88A2BD24EFB006CB06E /* ToyGeometryUtils.cpp in Sources */, B891A8A32BD24EFB006CB06E /* ToyScenarioHandler.cpp in Sources */, B891A84A2BD24EFB006CB06E /* AniMathUtils.cpp in Sources */, @@ -2420,7 +2412,6 @@ "-framework", "\"Reachability\"", "-framework", - "\"SingSound\"", "-framework", "\"SystemConfiguration\"", "-framework", @@ -2779,7 +2770,6 @@ "-framework", "\"Reachability\"", "-framework", - "\"SingSound\"", "-framework", "\"SystemConfiguration\"", "-framework", @@ -2965,7 +2955,6 @@ "-framework", "\"Reachability\"", "-framework", - "\"SingSound\"", "-framework", "\"SystemConfiguration\"", "-framework", diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index e25d225..525374b 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -14,8 +14,6 @@ import Flutter GeneratedPluginRegistrant.register(with: self) let controller : FlutterViewController = window?.rootViewController as! FlutterViewController - _ = VoiceXSMessageChannel(messager: controller.binaryMessenger) - _ = XSMessageMehtodChannel(message: controller.binaryMessenger); _ = GameMessageChannel(message: controller.binaryMessenger); return super.application(application, didFinishLaunchingWithOptions: launchOptions) diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h index e03a5ed..2a16ce7 100644 --- a/ios/Runner/Runner-Bridging-Header.h +++ b/ios/Runner/Runner-Bridging-Header.h @@ -3,9 +3,6 @@ #ifndef Runner_Bridging_Header_h #define Runner_Bridging_Header_h -// SingSound -#import - // UMCommon #import #import diff --git a/ios/Runner/VoiceXSMessageChannel.swift b/ios/Runner/VoiceXSMessageChannel.swift deleted file mode 100644 index fc02604..0000000 --- a/ios/Runner/VoiceXSMessageChannel.swift +++ /dev/null @@ -1,118 +0,0 @@ -// -// VoiceXSMessageChannel.swift -// Runner -// -// Created by MacBook Pro on 2023/6/28. -// - -import UIKit - -class VoiceXSMessageChannel: NSObject,SSOralEvaluatingManagerDelegate { - var resultData:Dictionary? - var channel:FlutterBasicMessageChannel? - init(messager:FlutterBinaryMessenger) { - super.init() - resultData = Dictionary() - self.setEvaluateConfig() - channel = FlutterBasicMessageChannel(name: "com.owEnglish.voiceXs.BasicMessageChannel", binaryMessenger: messager) - channel!.setMessageHandler { message, reply in - if let dict = message as? Dictionary { - self.evaluateVioce(dict: dict); - } - } - } - - //配置评测信息 - func setEvaluateConfig() { - let config = SSOralEvaluatingManagerConfig.init() - config.appKey = "a418" - config.secretKey = "1a16f31f2611bf32fb7b3fc38f5b2c81" - config.vad = true - config.frontTime = 3 - config.backTime = 3 - config.isOutputLog = false - SSOralEvaluatingManager.register(config) - SSOralEvaluatingManager.share().register(.line, userId: "321") - SSOralEvaluatingManager.share().delegate = self - } - - //开始评测 - func evaluateVioce(dict:Dictionary) { - let text = dict["word"] as! String - let type = dict["type"] as! Int - let userId = dict["userId"] as! String - let config = SSOralEvaluatingConfig() - config.oralContent = text - if (type == 0) { - config.oralType = .word - } else { - config.oralType = .sentence - } - config.userId = userId - SSOralEvaluatingManager.share().startEvaluateOral(with: config) - } - - //评测结果回调 - func evaluateResult() { - channel!.sendMessage(resultData) {(reply) in - self.resultData?.removeAll() - } - } - - //SSOralEvaluatingManagerDelegate - /** - 评测开始 - */ - func oralEvaluatingDidStart() { - print("评测开始") - } - - /** - 评测停止 - */ - func oralEvaluatingDidStop() { - print("评测结束") - } - - /** - 评测完成后的结果 - */ - func oralEvaluatingDidEnd(withResult result: [AnyHashable : Any]?, requestId request_id: String?) { - print("评测完成结果") - let resultDict:Dictionary = result?["result"] as! Dictionary - resultData!["result"] = "1" - //分数 - resultData!["overall"] = resultDict["overall"] - self.evaluateResult() - } - - /** - 评测失败回调 - */ - func oralEvaluatingDidEndError(_ error: Error?, requestId request_id: String?) { - print("评测失败") - resultData!["result"] = "0" - self.evaluateResult() - } - - /** - VAD(前置时间)超时回调 - */ - func oralEvaluatingDidVADFrontTimeOut() { - print("前置超时--->取消") - SSOralEvaluatingManager.share().cancelEvaluate() - if(resultData?.keys.count == 0) { - resultData!["result"] = "0" - self.evaluateResult(); - } - } - - /** - VAD(后置时间)超时回调 - */ - func oralEvaluatingDidVADBackTimeOut() { - print("后置超时--->结束") - ///结束回调 - SSOralEvaluatingManager.share().stopEvaluate(); - } -} diff --git a/ios/Runner/XSMessageMehtodChannel.swift b/ios/Runner/XSMessageMehtodChannel.swift deleted file mode 100644 index 4f9450a..0000000 --- a/ios/Runner/XSMessageMehtodChannel.swift +++ /dev/null @@ -1,170 +0,0 @@ -// -// XSMessageMehtodChannel.swift -// Runner -// -// Created by MacBook Pro on 2023/6/28. -// - -import UIKit - -class XSMessageMehtodChannel: NSObject,SSOralEvaluatingManagerDelegate { - var resultData:Dictionary? - var messageChannel:FlutterMethodChannel? - init(message:FlutterBinaryMessenger) { - super.init() - resultData = Dictionary() - messageChannel = FlutterMethodChannel.init(name: "wow_english/sing_sound_method_channel", binaryMessenger: message) - messageChannel!.setMethodCallHandler { call, result in - self.handle(call, result) - } - } - - //配置评测信息 - func setEvaluateConfig(dict:Dictionary) { - var appKey = "a418" - var secretKey = "c11163aa6c834a028da4a4b30955be99" - var service = "wss://api.cloud.ssapi.cn" - var userId = "guest" - var frontTime = "3" - var backTime = "3" - if (!dict.keys.isEmpty) { - appKey = dict["appKey"] as? String ?? "" - secretKey = dict["secretKey"] as? String ?? "" - service = dict["service"] as? String ?? "" - userId = dict["userId"] as? String ?? "guest" - frontTime = dict["frontTime"] as? String ?? "3" - backTime = dict["frontTime"] as? String ?? "3" - } - let config = SSOralEvaluatingManagerConfig.init() - config.vad = true - config.isOutputLog = false - config.appKey = appKey - config.secretKey = secretKey - config.frontTime = Double(frontTime)! - config.backTime = Double(backTime)! - config.setValue(service, forKey: "service") - SSOralEvaluatingManager.register(config) - SSOralEvaluatingManager.share().register(.line, userId: userId) - SSOralEvaluatingManager.share().delegate = self - } - - //开始评测 - func evaluateVoice(dict:Dictionary) { - let text = dict["word"] as? String ?? "" - let type = dict["type"] as? String ?? "0" - let userId = dict["userId"] as? String ?? "guest" - let config = SSOralEvaluatingConfig() - config.oralContent = text - if (type == "0") { - config.oralType = .word - } else { - config.oralType = .sentence - } - config.oralType = .kidSent - config.userId = userId - SSOralEvaluatingManager.share().startEvaluateOral(with: config) - } - - //开始评测(本地音频文件) - func evaluateLocalVoice(dict:Dictionary) { - let text = dict["word"] as? String ?? "" - let type = dict["type"] as? String ?? "0" - let userId = dict["userId"] as? String ?? "guest" - let voicePath = dict["voicePath"] as? String ?? "" - let config = SSOralEvaluatingConfig() - config.oralContent = text - if (type == "0") { - config.oralType = .word - } else { - config.oralType = .sentence - } - config.oralType = .sentence - config.userId = userId - SSOralEvaluatingManager.share().startEvaluateOral(withWavPath: voicePath, config: config) - } - - func handle(_ call: FlutterMethodCall,_ result: @escaping FlutterResult) { - if (call.method == "initVoiceSdk") { - self.setEvaluateConfig(dict:call.arguments as! Dictionary) - return - } - if (call.method == "startVoice") { - self.evaluateVoice(dict: call.arguments as! Dictionary) - return - } - - if (call.method == "startLocalVoice") { - self.evaluateLocalVoice(dict: call.arguments as! Dictionary) - return - } - - if (call.method == "stopVoice") { - SSOralEvaluatingManager.share().stopEvaluate(); - return - } - - if (call.method == "cancelVoice") { - SSOralEvaluatingManager.share().cancelEvaluate(); - messageChannel!.invokeMethod("voiceCancel",arguments: nil); - return; - } - } - - //评测结果回调 - func evaluateResult() { - messageChannel!.invokeMethod("voiceResult", arguments: resultData) - } - - //SSOralEvaluatingManagerDelegate - /** - 评测开始 - */ - func oralEvaluatingDidStart() { - print("评测开始") - messageChannel!.invokeMethod("voiceStart", arguments: nil) - } - - /** - 评测停止 - */ - func oralEvaluatingDidStop() { - print("评测结束") - messageChannel!.invokeMethod("voiceEnd",arguments: nil) - } - - /** - 评测完成后的结果 - */ - func oralEvaluatingDidEnd(withResult result: [AnyHashable : Any]?, requestId request_id: String?) { - let resultDict:Dictionary = result as! Dictionary - resultData! = resultDict; - self.evaluateResult() - } - - /** - 评测失败回调 - */ - func oralEvaluatingDidEndError(_ error: Error?, requestId request_id: String?) { - let nsError = error as? NSError - var map = Dictionary() - map["code"] = nsError?.code - map["message"] = error?.localizedDescription - messageChannel!.invokeMethod("voiceFail", arguments:map) - } - - /** - VAD(前置时间)超时回调 - */ - func oralEvaluatingDidVADFrontTimeOut() { - SSOralEvaluatingManager.share().cancelEvaluate() - messageChannel!.invokeMethod("voiceCancel",arguments: nil) - } - - /** - VAD(后置时间)超时回调 - */ - func oralEvaluatingDidVADBackTimeOut() { - ///结束回调 - SSOralEvaluatingManager.share().stopEvaluate(); - } -} diff --git a/lib/common/core/app_consts.dart b/lib/common/core/app_consts.dart index e5902c4..1cd43e1 100644 --- a/lib/common/core/app_consts.dart +++ b/lib/common/core/app_consts.dart @@ -1,5 +1,3 @@ -import '../request/basic_config.dart'; - class AppConsts { /// 隐私协议 static const String userPrivacyPolicyUrl = @@ -16,12 +14,7 @@ class AppConsts { static const String userTermSdkUrl = 'http://page.kouyuxingqiu.com/term_sdk.html'; - /// 先声SDK - static String xsAppKey = 'a418'; - static String xsAppSecretKey = BasicConfig.isTestDev - ? '1a16f31f2611bf32fb7b3fc38f5b2c81' - : 'c11163aa6c834a028da4a4b30955be99'; - static String xsAppService = BasicConfig.isTestDev - ? 'ws://trial.cloud.ssapi.cn:8080' - : '"wss://api.cloud.ssapi.cn'; + /// 驰声语音评测 SDK + static const String chivoxAppKey = '1718784836000171'; + static const String chivoxSecretKey = '3ce4e362e86b6c95e3b39a29fa486167'; } diff --git a/lib/common/speech/chivox_evaluation_channel.dart b/lib/common/speech/chivox_evaluation_channel.dart new file mode 100644 index 0000000..b697f60 --- /dev/null +++ b/lib/common/speech/chivox_evaluation_channel.dart @@ -0,0 +1,398 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:audio_session/audio_session.dart'; +import 'package:chivox_aiengine/chivox_aiengine.dart'; +import 'package:flutter/services.dart'; +import 'package:path_provider/path_provider.dart'; + +import '../core/app_consts.dart'; + +/// 将驰声 Flutter SDK 适配为原评测桥接的事件协议,业务页面无需感知 SDK 差异。 +class ChivoxEvaluationChannel { + ChivoxAiengine? _engine; + Future? _initializing; + Future Function(MethodCall)? _methodCallHandler; + int _sessionId = 0; + bool _active = false; + bool _stopping = false; + bool _stopNotified = false; + String? _recordFilePath; + + void setMethodCallHandler( + Future Function(MethodCall)? methodCallHandler) { + _methodCallHandler = methodCallHandler; + } + + Future invokeMethod(String method, [dynamic arguments]) async { + final params = _asStringMap(arguments); + switch (method) { + case 'initVoiceSdk': + await _initialize(); + break; + case 'startVoice': + await _startInnerRecorder(params); + break; + case 'startLocalVoice': + await _evaluateWaveFile(params); + break; + case 'stopVoice': + await _stop(); + break; + case 'cancelVoice': + await _cancel(notify: true); + break; + default: + throw MissingPluginException('Unsupported evaluation method: $method'); + } + return null; + } + + Future dispose() async { + _sessionId++; + await _cancel(notify: false); + final engine = _engine; + _engine = null; + _initializing = null; + await engine?.destroy(); + _methodCallHandler = null; + } + + Future _initialize() { + if (_engine != null) return Future.value(); + return _initializing ??= _createEngine().catchError((Object error) { + _initializing = null; + throw error; + }); + } + + Future _createEngine() async { + final supportDirectory = await getApplicationSupportDirectory(); + final resourceDirectory = Directory('${supportDirectory.path}/chivox'); + if (!await resourceDirectory.exists()) { + await resourceDirectory.create(recursive: true); + } + final provisionPath = await _copyAssetIfNeeded( + 'assets/chivox/aiengine.provision', + '${resourceDirectory.path}/aiengine.provision', + ); + final vadPath = await _copyAssetIfNeeded( + 'assets/chivox/vad.0.13.bin', + '${resourceDirectory.path}/vad.0.13.bin', + ); + + final config = jsonEncode({ + 'appKey': AppConsts.chivoxAppKey, + 'secretKey': AppConsts.chivoxSecretKey, + 'provision': provisionPath, + 'vad': { + 'enable': 1, + 'res': vadPath, + 'speechLowSeek': 125, + 'sampleRate': 16000, + 'strip': 0, + }, + 'cloud': {'enable': 1}, + }); + _engine = await ChivoxAiengine.create(config); + } + + Future _startInnerRecorder(Map params) async { + await _prepareAudioSession(); + await _startEvaluation( + params, + audioSourceBuilder: (recordFilePath) => { + 'srcType': 'innerRecorder', + 'innerRecorderParam': { + 'channel': 1, + 'sampleBytes': 2, + 'sampleRate': 16000, + 'saveFile': recordFilePath, + }, + }, + ); + } + + Future _evaluateWaveFile(Map params) async { + final wavePath = params['voicePath'] ?? ''; + if (wavePath.isEmpty || !await File(wavePath).exists()) { + await _emit('voiceFail', { + 'code': -1, + 'message': '录音文件不存在', + }); + return; + } + + await _startEvaluation( + params, + fallbackRecordPath: wavePath, + audioSourceBuilder: (_) => {'srcType': 'outerFeed'}, + afterStarted: (sessionId) async { + final pcmBytes = await _readWavePcm(File(wavePath)); + const chunkSize = 8192; + for (var offset = 0; + offset < pcmBytes.length && _isActiveSession(sessionId); + offset += chunkSize) { + final end = (offset + chunkSize).clamp(0, pcmBytes.length); + final chunk = Uint8List.sublistView(pcmBytes, offset, end); + await _engine?.feed(chunk, chunk.length); + } + if (_isActiveSession(sessionId)) await _stop(); + }, + ); + } + + Future _startEvaluation( + Map params, { + required Map Function(String recordFilePath) + audioSourceBuilder, + String? fallbackRecordPath, + Future Function(int sessionId)? afterStarted, + }) async { + try { + await _initialize(); + if (_active) await _cancel(notify: false); + + final text = _normalizeReferenceText(params['word'] ?? ''); + if (text.isEmpty) { + throw const FormatException('评测文本不能为空'); + } + final sessionId = ++_sessionId; + _active = true; + _stopping = false; + _stopNotified = false; + _recordFilePath = fallbackRecordPath ?? await _newRecordFilePath(); + + final request = jsonEncode({ + 'coreProvideType': 'cloud', + 'vad': { + 'vadEnable': fallbackRecordPath == null ? 1 : 0, + 'refDuration': 3, + 'speechLowSeek': 125, + }, + 'app': {'userId': params['userId'] ?? 'guest'}, + 'audio': { + 'audioType': 'wav', + 'channel': 1, + 'sampleBytes': 2, + 'sampleRate': 16000, + 'compress': 'speex', + }, + 'request': { + 'coreType': 'en.sent.score', + 'refText': text, + 'rank': 100, + 'attachAudioUrl': 0, + 'result': { + 'details': {'gop_adjust': 0} + }, + }, + }); + + final listener = ChivoxAiengineResultListener( + onEvalResult: (result) => _handleResult(sessionId, text, result), + onError: (result) => unawaited(_handleError(sessionId, result)), + onVad: (result) => _handleVad(sessionId, result), + ); + await _engine!.start( + audioSourceBuilder(_recordFilePath!), + request, + listener, + ); + if (!_isActiveSession(sessionId)) return; + await _emit('voiceStart', null); + await afterStarted?.call(sessionId); + } catch (error) { + _active = false; + _stopping = false; + try { + await _engine?.cancel(); + } catch (_) {} + await _emit('voiceFail', { + 'code': error is PlatformException ? error.code : -1, + 'message': error.toString(), + }); + } + } + + void _handleResult( + int sessionId, String referenceText, ChivoxAiengineResult result) { + if (!_isActiveSession(sessionId)) return; + try { + final raw = jsonDecode(result.text ?? '{}') as Map; + final resultJson = raw['result'] as Map? ?? const {}; + final detailsJson = resultJson['details'] as List? ?? const []; + final details = detailsJson + .whereType() + .map((detail) { + return { + 'char': detail['char']?.toString() ?? '', + 'score': _asScore(detail['score']), + }; + }) + .where((detail) => (detail['char'] as String).isNotEmpty) + .toList(); + + _active = false; + _stopping = false; + unawaited(_emit('voiceResult', { + 'result': { + 'overall': _asScore(resultJson['overall']), + 'details': details, + 'refText': referenceText, + }, + 'audioUrl': result.recFilePath ?? _recordFilePath ?? '', + })); + } catch (error) { + unawaited(_handleError(sessionId, null, error: error)); + } + } + + Future _handleError( + int sessionId, + ChivoxAiengineResult? result, { + Object? error, + }) async { + if (!_isActiveSession(sessionId)) return; + _active = false; + _stopping = false; + try { + await _engine?.cancel(); + } catch (_) {} + var code = -1; + var message = error?.toString() ?? result?.text ?? '评测失败'; + try { + final errorJson = jsonDecode(result?.text ?? '') as Map; + code = _asScore(errorJson['errId']); + message = errorJson['error']?.toString() ?? message; + } catch (_) {} + await _emit('voiceFail', {'code': code, 'message': message}); + } + + void _handleVad(int sessionId, ChivoxAiengineResult result) { + if (!_isActiveSession(sessionId) || _stopping) return; + try { + final vad = jsonDecode(result.text ?? '{}') as Map; + if (_asScore(vad['vad_status']) == 2) unawaited(_stop()); + } catch (_) {} + } + + Future _stop() async { + if (!_active || _stopping) return; + _stopping = true; + try { + await _engine?.stop(); + if (!_stopNotified) { + _stopNotified = true; + await _emit('voiceEnd', null); + } + } catch (error) { + _active = false; + _stopping = false; + _sessionId++; + try { + await _engine?.cancel(); + } catch (_) {} + await _emit('voiceFail', { + 'code': error is PlatformException ? error.code : -1, + 'message': error.toString(), + }); + } + } + + Future _cancel({required bool notify}) async { + final wasActive = _active; + _active = false; + _stopping = false; + _sessionId++; + try { + await _engine?.cancel(); + } catch (_) {} + if (notify && wasActive) await _emit('voiceCancel', null); + } + + bool _isActiveSession(int sessionId) => _active && sessionId == _sessionId; + + Future _prepareAudioSession() async { + final session = await AudioSession.instance; + await session.configure(AudioSessionConfiguration( + avAudioSessionCategory: AVAudioSessionCategory.playAndRecord, + avAudioSessionCategoryOptions: + AVAudioSessionCategoryOptions.defaultToSpeaker | + AVAudioSessionCategoryOptions.allowBluetooth, + avAudioSessionMode: AVAudioSessionMode.spokenAudio, + androidAudioAttributes: const AndroidAudioAttributes( + contentType: AndroidAudioContentType.speech, + usage: AndroidAudioUsage.voiceCommunication, + ), + androidAudioFocusGainType: AndroidAudioFocusGainType.gain, + androidWillPauseWhenDucked: true, + )); + await session.setActive(true); + } + + Future _newRecordFilePath() async { + final tempDirectory = await getTemporaryDirectory(); + final directory = Directory('${tempDirectory.path}/chivox_records'); + if (!await directory.exists()) await directory.create(recursive: true); + return '${directory.path}/${DateTime.now().millisecondsSinceEpoch}.wav'; + } + + Future _copyAssetIfNeeded(String assetPath, String targetPath) async { + final data = await rootBundle.load(assetPath); + final file = File(targetPath); + if (!await file.exists() || await file.length() != data.lengthInBytes) { + await file.writeAsBytes(data.buffer.asUint8List(), flush: true); + } + return file.path; + } + + Future _readWavePcm(File file) async { + final bytes = await file.readAsBytes(); + if (bytes.length < 12 || ascii.decode(bytes.sublist(0, 4)) != 'RIFF') { + return bytes; + } + var offset = 12; + final byteData = ByteData.sublistView(bytes); + while (offset + 8 <= bytes.length) { + final chunkName = + ascii.decode(bytes.sublist(offset, offset + 4), allowInvalid: true); + final chunkLength = byteData.getUint32(offset + 4, Endian.little); + final dataStart = offset + 8; + final dataEnd = (dataStart + chunkLength).clamp(0, bytes.length); + if (chunkName == 'data') { + return Uint8List.sublistView(bytes, dataStart, dataEnd); + } + offset = dataEnd + (chunkLength.isOdd ? 1 : 0); + } + throw const FormatException('无效的 WAV 文件'); + } + + Future _emit(String method, dynamic arguments) async { + await _methodCallHandler?.call(MethodCall(method, arguments)); + } + + Map _asStringMap(dynamic arguments) { + if (arguments is! Map) return const {}; + return arguments + .map((key, value) => MapEntry(key.toString(), value?.toString() ?? '')); + } + + int _asScore(dynamic value) { + if (value is num) return value.round(); + return double.tryParse(value?.toString() ?? '')?.round() ?? 0; + } + + String _normalizeReferenceText(String text) => text + .trim() + .replaceAll('’', "'") + .replaceAll('‘', "'") + .replaceAll('“', '"') + .replaceAll('”', '"') + .replaceAll(',', ',') + .replaceAll('。', '.') + .replaceAll('?', '?') + .replaceAll('!', '!'); +} diff --git a/lib/pages/practice/bloc/topic_picture_bloc.dart b/lib/pages/practice/bloc/topic_picture_bloc.dart index bb07bbd..4e4feb9 100644 --- a/lib/pages/practice/bloc/topic_picture_bloc.dart +++ b/lib/pages/practice/bloc/topic_picture_bloc.dart @@ -3,12 +3,12 @@ import 'dart:async'; import 'package:audioplayers/audioplayers.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:wow_english/common/request/dao/listen_dao.dart'; import 'package:wow_english/common/request/exception.dart'; +import 'package:wow_english/common/speech/chivox_evaluation_channel.dart'; import 'package:wow_english/models/course_process_entity.dart'; import 'package:wow_english/pages/section/subsection/base_section/bloc.dart'; import 'package:wow_english/pages/section/subsection/base_section/event.dart'; @@ -27,7 +27,8 @@ part 'topic_picture_event.dart'; part 'topic_picture_state.dart'; class TopicPictureBloc - extends BaseSectionBloc with WidgetsBindingObserver { + extends BaseSectionBloc + with WidgetsBindingObserver { final PageController pageController; final String courseLessonId; @@ -50,7 +51,7 @@ class TopicPictureBloc bool get isRecording => _isRecording; - late MethodChannel methodChannel; + final ChivoxEvaluationChannel methodChannel = ChivoxEvaluationChannel(); late AudioPlayer audioPlayer; @@ -66,7 +67,6 @@ class TopicPictureBloc on(_pageControllerChange); on(_voicePlayStateChange); on(_voiceXsResult); - on(_initVoiceSdk); on(_selectItemLoad); on(_selectItemReset); on(_requestData); @@ -74,7 +74,7 @@ class TopicPictureBloc on(_voiceXsStop); on(_questionVoicePlay); on(_onVoiceXsStateChange); - on((event, emit) { + on((event, emit) async { //音频播放器 audioPlayer = AudioPlayer(); audioPlayer.onPlayerStateChanged.listen((event) async { @@ -85,8 +85,6 @@ class TopicPictureBloc add(VoicePlayStateChangeEvent()); }); - methodChannel = - const MethodChannel('wow_english/sing_sound_method_channel'); methodChannel.setMethodCallHandler((call) async { if (call.method == 'voiceResult') { //评测结果 @@ -130,6 +128,8 @@ class TopicPictureBloc } }); + await methodChannel.invokeMethod('initVoiceSdk', {}); + WidgetsBinding.instance.addObserver(this); }); } @@ -153,11 +153,12 @@ class TopicPictureBloc } @override - Future close() { + Future close() async { pageController.dispose(); - audioPlayer.release(); - audioPlayer.dispose(); - _voiceXsCancel(); + await audioPlayer.release(); + await audioPlayer.dispose(); + await _voiceXsCancel(force: true); + await methodChannel.dispose(); WidgetsBinding.instance.removeObserver(this); return super.close(); } @@ -239,13 +240,7 @@ class TopicPictureBloc return answerList?.correct != 0; } - ///初始化SDK - _initVoiceSdk( - XSVoiceInitEvent event, Emitter emitter) async { - methodChannel.invokeMethod('initVoiceSdk', event.data); - } - - ///先声测试 + ///驰声测试 void _voiceXsStart( XSVoiceStartEvent event, Emitter emitter) async { await audioPlayer.stop(); @@ -253,7 +248,7 @@ class TopicPictureBloc bool result = await requestPermission( context, Permission.microphone, "录音", "用于开启录音,识别您的开口作答并给出反馈"); if (result) { - methodChannel.invokeMethod('startVoice', { + await methodChannel.invokeMethod('startVoice', { 'word': event.testWord, 'type': event.type, 'userId': event.userId.toString() @@ -264,17 +259,17 @@ class TopicPictureBloc ///终止评测 Future _voiceXsStop( XSVoiceStopEvent event, Emitter emitter) async { - methodChannel.invokeMethod('stopVoice'); + await methodChannel.invokeMethod('stopVoice'); } ///取消评测(用于处理退出页面后录音未停止等异常情况的保护操作) Future _voiceXsCancel({bool force = false}) async { if (_isRecording || force) { - methodChannel.invokeMethod('cancelVoice'); + await methodChannel.invokeMethod('cancelVoice'); } } - ///先声评测结果 + ///驰声评测结果 void _voiceXsResult( XSVoiceResultEvent event, Emitter emitter) async { _isRecording = false; @@ -286,7 +281,8 @@ class TopicPictureBloc final voiceResult = VoiceResultType.fromScore(score); if (voiceResult.lottieFilePath != null) { AudioPlayerUtil.getInstance().playAudio(voiceResult.audioType); - await showCheerRewardDialog(context, lottieFile: voiceResult.lottieFilePath!, onDismiss: () { + await showCheerRewardDialog(context, + lottieFile: voiceResult.lottieFilePath!, onDismiss: () { autoFlipPageByVoice(score); }); } else { diff --git a/lib/pages/practice/bloc/topic_picture_event.dart b/lib/pages/practice/bloc/topic_picture_event.dart index 11a4d65..e058d25 100644 --- a/lib/pages/practice/bloc/topic_picture_event.dart +++ b/lib/pages/practice/bloc/topic_picture_event.dart @@ -7,18 +7,12 @@ class InitBlocEvent extends TopicPictureEvent {} class RequestDataEvent extends TopicPictureEvent {} -///初始化先声SDK -class XSVoiceInitEvent extends TopicPictureEvent { - final Map data; - XSVoiceInitEvent(this.data); -} - ///开始评测 class XSVoiceStartEvent extends TopicPictureEvent { final String testWord; final String type; final String userId; - XSVoiceStartEvent(this.testWord,this.type,this.userId); + XSVoiceStartEvent(this.testWord, this.type, this.userId); } ///终止评测 @@ -30,7 +24,7 @@ class XSVoiceResultEvent extends TopicPictureEvent { XSVoiceResultEvent(this.message); } -///先声评测状态 +///驰声评测状态 class OnXSVoiceStateChangeEvent extends TopicPictureEvent {} ///音频播放状态变化 diff --git a/lib/pages/practice/topic_picture_page.dart b/lib/pages/practice/topic_picture_page.dart index fd896a6..41b4086 100644 --- a/lib/pages/practice/topic_picture_page.dart +++ b/lib/pages/practice/topic_picture_page.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:wow_english/common/core/app_consts.dart'; import 'package:wow_english/common/core/user_util.dart'; import 'package:wow_english/common/extension/string_extension.dart'; import 'package:wow_english/common/widgets/ow_image_widget.dart'; @@ -28,13 +27,7 @@ class TopicPicturePage extends StatelessWidget { create: (context) => TopicPictureBloc( context, PageController(), courseLessonId ?? '', moduleColor) ..add(InitBlocEvent()) - ..add(RequestDataEvent()) - ..add(XSVoiceInitEvent({ - 'appKey': AppConsts.xsAppKey, - 'service': AppConsts.xsAppService, - 'secretKey': AppConsts.xsAppSecretKey, - 'userId': UserUtil.getUser()!.id.toString(), - })), + ..add(RequestDataEvent()), child: _TopicPicturePage(), ); } @@ -310,8 +303,7 @@ class _TopicPicturePage extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ SpeakerWidget( - isPlaying: isCurrentPage && - bloc.isAudioPlaying(), + isPlaying: isCurrentPage && bloc.isAudioPlaying(), // 控制动画播放 width: 32.w, height: 32.w, @@ -391,8 +383,7 @@ class _TopicPicturePage extends StatelessWidget { child: Column( children: [ SpeakerWidget( - isPlaying: isCurrentPage && - bloc.isAudioPlaying(), + isPlaying: isCurrentPage && bloc.isAudioPlaying(), width: 32.w, height: 32.w, onTap: () { @@ -504,8 +495,7 @@ class _TopicPicturePage extends StatelessWidget { Row( children: [ SpeakerWidget( - isPlaying: isCurrentPage && - bloc.isAudioPlaying(), + isPlaying: isCurrentPage && bloc.isAudioPlaying(), // 控制动画播放 isClickable: !bloc.isRecording, // 控制是否可点击 diff --git a/lib/pages/reading/bloc/reading_bloc.dart b/lib/pages/reading/bloc/reading_bloc.dart index 1b2a3e2..58ab174 100644 --- a/lib/pages/reading/bloc/reading_bloc.dart +++ b/lib/pages/reading/bloc/reading_bloc.dart @@ -1,7 +1,6 @@ import 'package:audioplayers/audioplayers.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; @@ -15,6 +14,7 @@ import '../../../common/core/user_util.dart'; import '../../../common/permission/permissionRequester.dart'; import '../../../common/request/dao/listen_dao.dart'; import '../../../common/request/exception.dart'; +import '../../../common/speech/chivox_evaluation_channel.dart'; import '../../../common/utils/show_star_reward_dialog.dart'; import '../../../models/course_process_entity.dart'; import '../../../models/singsound_result_detail_entity.dart'; @@ -83,7 +83,7 @@ class ReadingPageBloc VoicePlayState get voicePlayState => _voicePlayState; - late MethodChannel methodChannel; + final ChivoxEvaluationChannel methodChannel = ChivoxEvaluationChannel(); late AudioPlayer audioPlayer; @@ -91,7 +91,8 @@ class ReadingPageBloc final Color? moduleColor; - ReadingPageBloc(this.context, this.pageController, this.courseLessonId, this.moduleColor) + ReadingPageBloc( + this.context, this.pageController, this.courseLessonId, this.moduleColor) : super(ReadingPageInitial()) { on(_pageControllerChange); on(_playModeChange); @@ -99,7 +100,7 @@ class ReadingPageBloc // _currentPage = pageController.page!.round(); // }); on(_requestData); - on((event, emit) { + on((event, emit) async { //音频播放器 audioPlayer = AudioPlayer(); audioPlayer.onPlayerStateChanged.listen((event) async { @@ -130,9 +131,6 @@ class ReadingPageBloc add(VoicePlayStateChangeEvent()); }); - methodChannel = - const MethodChannel('wow_english/sing_sound_method_channel'); - methodChannel.invokeMethod('initVoiceSdk', {}); //初始化评测 methodChannel.setMethodCallHandler((call) async { Log.d( "setMethodCallHandler method=${call.method} arguments=${call.arguments}"); @@ -179,10 +177,10 @@ class ReadingPageBloc return; } }); + await methodChannel.invokeMethod('initVoiceSdk', {}); //初始化评测 }); on(_voicePlayStateChange); on(_playOriginalAudio); - on(_initVoiceSdk); on(_voiceXsStart); on(_voiceXsStop); on(_voiceXsResult); @@ -192,11 +190,12 @@ class ReadingPageBloc } @override - Future close() { + Future close() async { pageController.dispose(); - audioPlayer.release(); - audioPlayer.dispose(); - _voiceXsCancel(force: true); + await audioPlayer.release(); + await audioPlayer.dispose(); + await _voiceXsCancel(force: true); + await methodChannel.dispose(); return super.close(); } @@ -295,8 +294,11 @@ class ReadingPageBloc Future _playAudio(String? audioUrl) async { if (audioUrl != null && audioUrl.isNotEmpty) { try { - await audioPlayer.play(UrlSource(audioUrl), - balance: 0.0, ctx: AudioContext()); + final source = + audioUrl.startsWith('http://') || audioUrl.startsWith('https://') + ? UrlSource(audioUrl) + : DeviceFileSource(audioUrl); + await audioPlayer.play(source, balance: 0.0, ctx: AudioContext()); } catch (e) { Log.d('_playAudio error: $e'); } @@ -329,8 +331,7 @@ class ReadingPageBloc } else { List? wordList = currentPageData()?.word?.split(RegExp(r'\s+')); resultDetails = (wordList ?? []) - .map( - (word) => SingsoundResultDetailEntity.withCharAndScore(word, 0)) + .map((word) => SingsoundResultDetailEntity.withCharAndScore(word, 0)) .toList(); } List textSpans = resultDetails.asMap().entries.map((entry) { @@ -372,13 +373,7 @@ class ReadingPageBloc } } - ///初始化SDK - _initVoiceSdk( - XSVoiceInitEvent event, Emitter emitter) async { - methodChannel.invokeMethod('initVoiceSdk', event.data); - } - - ///先声测试 + ///驰声测试 void _voiceXsStart( XSVoiceStartEvent event, Emitter emitter) async { await _stopAudio(); @@ -391,7 +386,7 @@ class ReadingPageBloc bool result = await requestPermission( context, Permission.microphone, "录音", "用于开启录音,识别您的开口作答并给出反馈"); if (result) { - methodChannel.invokeMethod('startVoice', { + await methodChannel.invokeMethod('startVoice', { 'word': content, 'type': '0', 'userId': UserUtil.getUser()?.id.toString() @@ -409,17 +404,17 @@ class ReadingPageBloc // 提取 score 和 char 字段 List detailEntities = []; for (var detail in resultDetailsJsons) { - int score = detail['score'] as int; + int score = (detail['score'] as num?)?.round() ?? 0; String char = detail['char'] as String; detailEntities .add(SingsoundResultDetailEntity.withCharAndScore(char, score)); } ///todo 后面可以考虑要不要传自己的服务器 - final recordFileUrl = args['audioUrl'].toString(); + final recordFileUrl = args['audioUrl']?.toString() ?? ''; int score = int.parse(overall); currentPageData()?.recordScore = overall; - currentPageData()?.recordUrl = args['audioUrl'] + '.mp3'; + currentPageData()?.recordUrl = recordFileUrl; currentPageData()?.resultDetails = detailEntities; add(OnXSVoiceStateChangeEvent()); @@ -427,7 +422,8 @@ class ReadingPageBloc if (voiceResult.lottieFilePath != null) { AudioPlayerUtil.getInstance().playAudio(voiceResult.audioType); - await showCheerRewardDialog(context, lottieFile: voiceResult.lottieFilePath!, onDismiss: () async { + await showCheerRewardDialog(context, + lottieFile: voiceResult.lottieFilePath!, onDismiss: () async { await actionAfterRecord(); }); } else { @@ -458,14 +454,14 @@ class ReadingPageBloc ///终止评测 void _voiceXsStop( XSVoiceStopEvent event, Emitter emitter) async { - methodChannel.invokeMethod('stopVoice'); + await methodChannel.invokeMethod('stopVoice'); } ///取消评测(用于处理退出页面后录音未停止等异常情况的保护操作) - void _voiceXsCancel({bool force = false}) { + Future _voiceXsCancel({bool force = false}) async { Log.d("取消评测 _voiceXsCancel _isRecording=$_isRecording"); if (_isRecording || force) { - methodChannel.invokeMethod('cancelVoice'); + await methodChannel.invokeMethod('cancelVoice'); } } diff --git a/lib/pages/reading/bloc/reading_event.dart b/lib/pages/reading/bloc/reading_event.dart index 2adfac8..d3f3429 100644 --- a/lib/pages/reading/bloc/reading_event.dart +++ b/lib/pages/reading/bloc/reading_event.dart @@ -22,19 +22,13 @@ class PlayOriginalAudioEvent extends ReadingPageEvent { PlayOriginalAudioEvent(this.url); } -///初始化先声SDK -class XSVoiceInitEvent extends ReadingPageEvent { - final Map data; - XSVoiceInitEvent(this.data); -} - ///评测结果 class XSVoiceResultEvent extends ReadingPageEvent { final dynamic message; XSVoiceResultEvent(this.message); } -///先声测试 +///驰声测试 class XSVoiceStartEvent extends ReadingPageEvent { final String content; final String type; @@ -42,10 +36,10 @@ class XSVoiceStartEvent extends ReadingPageEvent { XSVoiceStartEvent(this.content, this.type, this.userId); } -///先声评测停止 +///驰声评测停止 class XSVoiceStopEvent extends ReadingPageEvent {} -///先声评测状态 +///驰声评测状态 class OnXSVoiceStateChangeEvent extends ReadingPageEvent {} ///音频播放状态 diff --git a/lib/pages/reading/reading_page.dart b/lib/pages/reading/reading_page.dart index bcb02c7..54e3939 100644 --- a/lib/pages/reading/reading_page.dart +++ b/lib/pages/reading/reading_page.dart @@ -7,7 +7,6 @@ import 'package:wow_english/pages/reading/widgets/ReadingModeType.dart'; import 'package:wow_english/pages/reading/widgets/reading_dialog_widget.dart'; import 'package:wow_english/route/route.dart'; -import '../../common/core/app_consts.dart'; import '../../common/core/user_util.dart'; import '../../common/widgets/recorder_widget.dart'; import '../../common/widgets/speaker_widget.dart'; @@ -29,13 +28,7 @@ class ReadingPage extends StatelessWidget { create: (_) => ReadingPageBloc( context, PageController(), courseLessonId ?? '', moduleColor) ..add(InitBlocEvent()) - ..add(RequestDataEvent()) - ..add(XSVoiceInitEvent({ - 'appKey': AppConsts.xsAppKey, - 'service': AppConsts.xsAppService, - 'secretKey': AppConsts.xsAppSecretKey, - 'userId': UserUtil.getUser()!.id.toString(), - })), + ..add(RequestDataEvent()), child: _ReadingPage(), ); } diff --git a/lib/pages/repeataftercontent/bloc/repeat_after_content_bloc.dart b/lib/pages/repeataftercontent/bloc/repeat_after_content_bloc.dart index 4c3a7da..8711063 100644 --- a/lib/pages/repeataftercontent/bloc/repeat_after_content_bloc.dart +++ b/lib/pages/repeataftercontent/bloc/repeat_after_content_bloc.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:audio_session/audio_session.dart'; import 'package:flutter/cupertino.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_sound/flutter_sound.dart'; import 'package:path_provider/path_provider.dart'; @@ -12,58 +11,70 @@ import 'package:wow_english/common/request/dao/listen_dao.dart'; import 'package:wow_english/route/route.dart'; import '../../../common/dialogs/show_dialog.dart'; import '../../../common/request/exception.dart'; +import '../../../common/speech/chivox_evaluation_channel.dart'; import '../../../models/read_content_entity.dart'; import '../../../utils/loading.dart'; import '../../../utils/toast_util.dart'; - part 'repeat_after_content_event.dart'; part 'repeat_after_content_state.dart'; enum VoiceRecordState { ///未知 voiceRecordUnkonw, + ///开始录音 voiceRecordStat, + ///正在录音 voiceRecording, + ///录音结束 voiceRecordEnd } -///先声测评状态 +///驰声测评状态 enum XSVoiceCheckState { ///未知 unKnow, + ///测评开始 start, + ///评测结果 result, + ///测评结束 stop, } -class RepeatAfterContentBloc extends Bloc { - +class RepeatAfterContentBloc + extends Bloc { final String courseLessonId; /// 是否正在播放视频 bool _videoPlaying = true; bool get videoPlaying => _videoPlaying; + /// 是否正在录音 bool _isRecord = false; bool get isRecord => _isRecord; - /// 先声评测状态 + + /// 驰声评测状态 XSVoiceCheckState _xSCheckState = XSVoiceCheckState.unKnow; XSVoiceCheckState get xSCheckState => _xSCheckState; + /// 评测结果 Map? _voiceTestResult; Map? get voiceTestResult => _voiceTestResult; + /// 录音的次数 int _recordNumber = 0; + /// 录音文件地址 String _path = ''; String get path => _path; + /// 当前播放的视频位置 int _currentPlayIndex = 0; int get currentPlayIndex => _currentPlayIndex; @@ -74,17 +85,18 @@ class RepeatAfterContentBloc extends Bloc? _entityList; - List? get entityList => _entityList ; + List? get entityList => _entityList; /// 方法 - late MethodChannel methodChannel; + final ChivoxEvaluationChannel methodChannel = ChivoxEvaluationChannel(); ///录音 late FlutterSoundRecorder _soundRecorder; late FlutterSoundPlayer _soundPlayer; // StreamSubscription? _soundPlayerListen; - RepeatAfterContentBloc(this.courseLessonId) : super(RepeatAfterContentInitial()) { + RepeatAfterContentBloc(this.courseLessonId) + : super(RepeatAfterContentInitial()) { on(_voiceRecordStateChange); on(_postFollowReadContent); on(_changeVideoPlayIndex); @@ -93,7 +105,6 @@ class RepeatAfterContentBloc extends Bloc(_starRecordVoice); on(_stopRecordVoice); on(_voiceXsResult); - on(_initVoiceSdk); on(_requestData); on(_voiceXsTest); on(_voiceXsStop); @@ -102,17 +113,19 @@ class RepeatAfterContentBloc extends Bloc close() { - _releaseFlauto(); - _voiceXsCancel(); + Future close() async { + await _releaseFlauto(); + await _voiceXsCancel(); + await methodChannel.dispose(); return super.close(); } ///初始化功能 - void _initBlocData(InitBlocEvent event, Emitter emitter) async { - methodChannel = const MethodChannel('wow_english/sing_sound_method_channel'); + void _initBlocData( + InitBlocEvent event, Emitter emitter) async { methodChannel.setMethodCallHandler((call) async { - if (call.method == 'voiceResult') {//评测结果 + if (call.method == 'voiceResult') { + //评测结果 add(XSVoiceResultEvent(call.arguments)); add(PostFollowReadContentEvent()); return; @@ -121,28 +134,36 @@ class RepeatAfterContentBloc extends Bloc _init() async { await _soundRecorder.openRecorder(); - await _soundRecorder.setSubscriptionDuration(const Duration(milliseconds: 10)); + await _soundRecorder + .setSubscriptionDuration(const Duration(milliseconds: 10)); //设置音频 final session = await AudioSession.instance; await session.configure(AudioSessionConfiguration( avAudioSessionCategory: AVAudioSessionCategory.playAndRecord, avAudioSessionCategoryOptions: - AVAudioSessionCategoryOptions.allowBluetooth | - AVAudioSessionCategoryOptions.defaultToSpeaker, + AVAudioSessionCategoryOptions.allowBluetooth | + AVAudioSessionCategoryOptions.defaultToSpeaker, avAudioSessionMode: AVAudioSessionMode.spokenAudio, avAudioSessionRouteSharingPolicy: - AVAudioSessionRouteSharingPolicy.defaultPolicy, + AVAudioSessionRouteSharingPolicy.defaultPolicy, avAudioSessionSetActiveOptions: AVAudioSessionSetActiveOptions.none, androidAudioAttributes: const AndroidAudioAttributes( contentType: AndroidAudioContentType.speech, @@ -154,11 +175,13 @@ class RepeatAfterContentBloc extends Bloc emitter) async { + void _requestData( + RequestDataEvent event, Emitter emitter) async { try { await loading(() async { _entityList = await ListenDao.readContent(courseLessonId); @@ -166,107 +189,101 @@ class RepeatAfterContentBloc extends Bloc emitter) async { + void _postFollowReadContent(PostFollowReadContentEvent event, + Emitter emitter) async { try { ReadContentEntity entity = _entityList![_currentPlayIndex]!; - await ListenDao.followResult(_recordNumber.toString(),entity.id); + await ListenDao.followResult(_recordNumber.toString(), entity.id); } catch (e) { - if (e is ApiException) { - - } + if (e is ApiException) {} } } - void _videoPlayStateChange(VideoPlayChangeEvent event,Emitter emitter) async { + void _videoPlayStateChange(VideoPlayChangeEvent event, + Emitter emitter) async { _videoPlaying = !_videoPlaying; emitter(VideoPlayChangeState()); } - void _voiceRecord(VoiceRecordEvent event,Emitter emitter) async { + void _voiceRecord( + VoiceRecordEvent event, Emitter emitter) async { _isRecord = !_isRecord; emitter(VoiceRecordChangeState()); } - void _voiceRecordStateChange(VoiceRecordStateChangeEvent event,Emitter emitter) async { + void _voiceRecordStateChange(VoiceRecordStateChangeEvent event, + Emitter emitter) async { _voiceRecordState = event.voiceRecordState; emitter(VoiceRecordStateChange()); } - - _initVoiceSdk(XSVoiceInitEvent event,Emitter emitter) async { - methodChannel.invokeMethod('initVoiceSdk',event.data); - } - - ///先声测试 - void _voiceXsTest(XSVoiceTestEvent event,Emitter emitter) async { + ///驰声测试 + void _voiceXsTest( + XSVoiceTestEvent event, Emitter emitter) async { _recordNumber += 1; _xSCheckState = XSVoiceCheckState.start; emitter(XSVoiceTestState()); - await methodChannel.invokeMethod( - 'startLocalVoice', - { - 'type':event.type, - 'word':event.testWord, - 'voicePath':_path, - 'userId':event.userId.toString() - } - ); + await methodChannel.invokeMethod('startLocalVoice', { + 'type': event.type, + 'word': event.testWord, + 'voicePath': _path, + 'userId': event.userId.toString() + }); } ///终止评测 - void _voiceXsStop(XSVoiceStopEvent event,Emitter emitter) async { - methodChannel.invokeMethod('stopVoice'); + void _voiceXsStop( + XSVoiceStopEvent event, Emitter emitter) async { + await methodChannel.invokeMethod('stopVoice'); } ///取消评测(用于处理退出页面后录音未停止等异常情况的保护操作) - void _voiceXsCancel() { - methodChannel.invokeMethod('cancelVoice'); + Future _voiceXsCancel() async { + await methodChannel.invokeMethod('cancelVoice'); } - ///先声评测结果 - void _voiceXsResult(XSVoiceResultEvent event,Emitter emitter) async { + ///驰声评测结果 + void _voiceXsResult(XSVoiceResultEvent event, + Emitter emitter) async { final Map args = event.message as Map; final result = args['result'] as Map; final overall = result['overall'].toString(); - _voiceTestResult = {'overall':overall}; + _voiceTestResult = {'overall': overall}; _xSCheckState = XSVoiceCheckState.result; emitter(XSVoiceTestState()); } ///播放声音 - void _recordeVoicePlay(RecordeVoicePlayEvent event,Emitter emitter) async { + void _recordeVoicePlay(RecordeVoicePlayEvent event, + Emitter emitter) async { if (await _fileExists(_path)) { if (_soundPlayer.isPlaying) { _soundPlayer.stopPlayer(); } await _soundPlayer.startPlayer( - fromURI: path, - codec: Codec.pcm16WAV, - whenFinished: (){ - - } - ); + fromURI: path, codec: Codec.pcm16WAV, whenFinished: () {}); } } ///更改播放的视频 - void _changeVideoPlayIndex(ChangeVideoPlayIndexEvent event,Emitter emitter) async { + void _changeVideoPlayIndex(ChangeVideoPlayIndexEvent event, + Emitter emitter) async { if (_entityList == null || _entityList!.isEmpty) { return; } if (event.isNext) { - if (_currentPlayIndex < _entityList!.length-1) { + if (_currentPlayIndex < _entityList!.length - 1) { _currentPlayIndex++; } } else { - if (_currentPlayIndex >0) { + if (_currentPlayIndex > 0) { _currentPlayIndex--; } } @@ -274,7 +291,8 @@ class RepeatAfterContentBloc extends Bloc emitter) async { + void _starRecordVoice(StarRecordVoiceEvent event, + Emitter emitter) async { try { await getPermissionStatus().then((value) async { if (!value) { @@ -302,7 +320,8 @@ class RepeatAfterContentBloc extends Bloc emitter) async { + void _stopRecordVoice(StopRecordVoiceEvent event, + Emitter emitter) async { debugPrint('=====> 停止录音'); await _soundRecorder.stopRecorder(); _voiceRecordState = VoiceRecordState.voiceRecordEnd; @@ -326,9 +345,7 @@ class RepeatAfterContentBloc extends Bloc RepeatAfterContentBloc(videoFollowReadId ??'') + create: (context) => RepeatAfterContentBloc(videoFollowReadId ?? '') ..add(InitBlocEvent()) - ..add(RequestDataEvent()) - ..add(XSVoiceInitEvent( - { - 'appKey':AppConsts.xsAppKey, - 'service':AppConsts.xsAppService, - 'secretKey':AppConsts.xsAppSecretKey, - } - )), + ..add(RequestDataEvent()), child: _RepeatAfterContentPage(), ); } @@ -38,13 +30,17 @@ class RepeatAfterContentPage extends StatelessWidget { class _RepeatAfterContentPage extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocListener( - listener: (context,state){ + return BlocListener( + listener: (context, state) { final bloc = BlocProvider.of(context); - if (state is VoiceRecordStateChange) {//录音状态回调 - if (bloc.voiceRecordState == VoiceRecordState.voiceRecordEnd) {//声音录制结束 - ReadContentEntity? readContentEntity = bloc.entityList?[bloc.currentPlayIndex]; - bloc.add(XSVoiceTestEvent(readContentEntity?.word??'','0',UserUtil.getUser()!.id.toString())); + if (state is VoiceRecordStateChange) { + //录音状态回调 + if (bloc.voiceRecordState == VoiceRecordState.voiceRecordEnd) { + //声音录制结束 + ReadContentEntity? readContentEntity = + bloc.entityList?[bloc.currentPlayIndex]; + bloc.add(XSVoiceTestEvent(readContentEntity?.word ?? '', '0', + UserUtil.getUser()!.id.toString())); } return; } @@ -53,224 +49,223 @@ class _RepeatAfterContentPage extends StatelessWidget { ); } - - Widget _repeatAfterContentView() => BlocBuilder(builder: (context,state){ - final bloc = BlocProvider.of(context); - final String videoUrl = bloc.entityList?.first?.videoUrl??''; - return Container( - color: Colors.white, - child: SafeArea( - child: Stack( - children: [ - ///返回 - Positioned( - child: GestureDetector( - onTap: () { - showDialog( - context: context, - builder: (context){ - return RepeatAfterContentDialog( (){ - popPage(); - }); - }); - // popPage(); - }, - child: Image.asset( - 'back_around'.assetPng, - height: 40.h, - width: 40.w, - ), - ), - ), - ///左侧视频区 - Positioned( - top: 40.h, - left: 20.w, - child: Container( - width: 285.w, - height: 299.h, - padding: EdgeInsets.symmetric(horizontal: 50.w,vertical: 50.h), - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage('video_background'.assetPng), - fit: BoxFit.fill + Widget _repeatAfterContentView() => + BlocBuilder( + builder: (context, state) { + final bloc = BlocProvider.of(context); + final String videoUrl = bloc.entityList?.first?.videoUrl ?? ''; + return Container( + color: Colors.white, + child: SafeArea( + child: Stack( + children: [ + ///返回 + Positioned( + child: GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (context) { + return RepeatAfterContentDialog(() { + popPage(); + }); + }); + // popPage(); + }, + child: Image.asset( + 'back_around'.assetPng, + height: 40.h, + width: 40.w, + ), ), ), - child: videoUrl.isEmpty?Container(): RepeatVideoWidget(videoUrl: bloc.entityList?.first?.videoUrl,videoUrls: bloc.entityList??[],), - ), - ), - ///右侧操作区 - Positioned( - top: 40.h, - left: 331.w, - child: Container( - width: 240.w, - height: 299.h, - padding: EdgeInsets.only( - left: 67.w, - bottom: 40.h - ), - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage('light_ground'.assetPng), - fit: BoxFit.fill + + ///左侧视频区 + Positioned( + top: 40.h, + left: 20.w, + child: Container( + width: 285.w, + height: 299.h, + padding: + EdgeInsets.symmetric(horizontal: 50.w, vertical: 50.h), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('video_background'.assetPng), + fit: BoxFit.fill), + ), + child: videoUrl.isEmpty + ? Container() + : RepeatVideoWidget( + videoUrl: bloc.entityList?.first?.videoUrl, + videoUrls: bloc.entityList ?? [], + ), ), ), - child: bloc.isRecord?_buildLongPressWidget():_buildPlayVideoWidget(), - ), - ), - ///连接 - Positioned( - top: 59.h, - left: 274.w, - child: Container( - width: 87.w, - height: 240.h, - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage('and_book'.assetPng), - fit: BoxFit.fill + + ///右侧操作区 + Positioned( + top: 40.h, + left: 331.w, + child: Container( + width: 240.w, + height: 299.h, + padding: EdgeInsets.only(left: 67.w, bottom: 40.h), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('light_ground'.assetPng), + fit: BoxFit.fill), + ), + child: bloc.isRecord + ? _buildLongPressWidget() + : _buildPlayVideoWidget(), ), ), - ), - ), - ///跟读 - Positioned( - top: 16.h, - left: 65.w, - child: Container( - width: 185.w, - height: 48.h, - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage('title_ground'.assetPng), - fit: BoxFit.fill + + ///连接 + Positioned( + top: 59.h, + left: 274.w, + child: Container( + width: 87.w, + height: 240.h, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('and_book'.assetPng), + fit: BoxFit.fill), + ), ), ), - alignment: Alignment.center, - child: Text( - 'read title', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, - fontSize: 21.sp + + ///跟读 + Positioned( + top: 16.h, + left: 65.w, + child: Container( + width: 185.w, + height: 48.h, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('title_ground'.assetPng), + fit: BoxFit.fill), + ), + alignment: Alignment.center, + child: Text( + 'read title', + textAlign: TextAlign.center, + style: TextStyle(color: Colors.white, fontSize: 21.sp), + ), ), ), - ), + ], ), - ], - ), - ), - ); - }); + ), + ); + }); ///播放中 Widget _buildPlayVideoWidget() { - return BlocBuilder( + return BlocBuilder( buildWhen: (previous, current) { - if (current is ChangeVideoPlayIndexEvent) { - return true; - } - return false; - }, - builder: (context,state){ - final bloc = BlocProvider.of(context); - return Column( - mainAxisAlignment: MainAxisAlignment.end, + if (current is ChangeVideoPlayIndexEvent) { + return true; + } + return false; + }, builder: (context, state) { + final bloc = BlocProvider.of(context); + return Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Row( + children: [ + IconButton( + onPressed: () => bloc.add(ChangeVideoPlayIndexEvent(false)), + icon: Image.asset( + 'previous'.assetPng, + height: 23.h, + width: 23.w, + )), + IconButton( + onPressed: () => bloc.add(VideoPlayChangeEvent()), + icon: Image.asset( + 'video_pause'.assetPng, + height: 50.h, + width: 50.h, + )), + IconButton( + onPressed: () => bloc.add(ChangeVideoPlayIndexEvent(true)), + icon: Image.asset( + 'next'.assetPng, + height: 23.h, + width: 23.w, + )) + ], + ), + Row( children: [ - Row( + SizedBox( + height: 23.h, + width: 23.w, + ), + 10.horizontalSpace, + Column( children: [ + 20.verticalSpace, IconButton( - onPressed: () => bloc.add(ChangeVideoPlayIndexEvent(false)), - icon: Image.asset( - 'previous'.assetPng, - height: 23.h, - width: 23.w, - ) - ), - IconButton( - onPressed:() => bloc.add(VideoPlayChangeEvent()), - icon: Image.asset( - 'video_pause'.assetPng, - height: 50.h, - width: 50.h, - ) - ), - IconButton( - onPressed: () => bloc.add(ChangeVideoPlayIndexEvent(true)), + onPressed: () { + if (bloc.videoPlaying) { + showToast('视频正在播放中'); + return; + } + bloc.add(VoiceRecordEvent()); + }, icon: Image.asset( - 'next'.assetPng, - height: 23.h, - width: 23.w, - ) + 'video_record'.assetPng, + height: 53.h, + width: 53.w, + )), + Text( + '录音', + style: TextStyle( + color: const Color(0xFF333333), fontSize: 14.sp), ) ], ), - Row( - children: [ - SizedBox( - height: 23.h, - width: 23.w, - ), - 10.horizontalSpace, - Column( - children: [ - 20.verticalSpace, - IconButton( - onPressed: () { - if (bloc.videoPlaying) { - showToast('视频正在播放中'); - return; - } - bloc.add(VoiceRecordEvent()); - }, - icon: Image.asset( - 'video_record'.assetPng, - height: 53.h, - width: 53.w, - ) - ), - Text( - '录音', - style: TextStyle( - color: const Color(0xFF333333), - fontSize: 14.sp - ), - ) - ], - ), - // Container( - // height: 22.h, - // width: 37.w, - // decoration: BoxDecoration( - // color: const Color(0xFF56CE5F), - // borderRadius: BorderRadius.circular(10.r) - // ), - // child: Text( - // '1.0x', - // textAlign: TextAlign.center, - // style: TextStyle( - // color: Colors.white, - // fontSize: 12.sp - // ), - // ), - // ) - ], - ) + // Container( + // height: 22.h, + // width: 37.w, + // decoration: BoxDecoration( + // color: const Color(0xFF56CE5F), + // borderRadius: BorderRadius.circular(10.r) + // ), + // child: Text( + // '1.0x', + // textAlign: TextAlign.center, + // style: TextStyle( + // color: Colors.white, + // fontSize: 12.sp + // ), + // ), + // ) ], - ); - }); + ) + ], + ); + }); } ///长按录音 - Widget _buildLongPressWidget() => BlocBuilder( - builder: (context,state){ + Widget _buildLongPressWidget() => + BlocBuilder( + builder: (context, state) { final bloc = BlocProvider.of(context); final voiceResult = bloc.voiceTestResult; Color color; - if (int.parse(voiceResult?['overall'].toString()??'0') >= 60 || int.parse(voiceResult?['overall'].toString()??'0') <= 75) { + if (int.parse(voiceResult?['overall'].toString() ?? '0') >= 60 || + int.parse(voiceResult?['overall'].toString() ?? '0') <= 75) { color = const Color(0xFFFF0000); - } else if (int.parse(voiceResult?['overall'].toString()??'0') > 75 || int.parse(voiceResult?['overall'].toString()??'0') <= 85) { + } else if (int.parse(voiceResult?['overall'].toString() ?? '0') > 75 || + int.parse(voiceResult?['overall'].toString() ?? '0') <= 85) { color = const Color(0xFFFFCC00); } else { color = const Color(0xFF40E04B); @@ -279,11 +274,15 @@ class _RepeatAfterContentPage extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.end, children: [ Offstage( - offstage:!(bloc.voiceRecordState == VoiceRecordState.voiceRecordEnd && bloc.xSCheckState == XSVoiceCheckState.result), + offstage: + !(bloc.voiceRecordState == VoiceRecordState.voiceRecordEnd && + bloc.xSCheckState == XSVoiceCheckState.result), child: Column( children: [ Offstage( - offstage:int.parse(voiceResult?['overall'].toString()??'0') > 60, + offstage: + int.parse(voiceResult?['overall'].toString() ?? '0') > + 60, child: Image.asset( 'sorrow_face'.assetPng, height: 46.h, @@ -291,57 +290,52 @@ class _RepeatAfterContentPage extends StatelessWidget { ), ), Offstage( - offstage: int.parse(voiceResult?['overall'].toString()??'0') < 60, + offstage: + int.parse(voiceResult?['overall'].toString() ?? '0') < + 60, child: Container( height: 45.h, width: 45.h, alignment: Alignment.center, decoration: BoxDecoration( color: color, - borderRadius: BorderRadius.circular(22.5.r) - ), + borderRadius: BorderRadius.circular(22.5.r)), child: Text( - voiceResult?['overall'].toString()??'0', + voiceResult?['overall'].toString() ?? '0', textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, - fontSize: 17.sp - ), + style: TextStyle(color: Colors.white, fontSize: 17.sp), ), ), ), IconButton( - onPressed: (){ + onPressed: () { bloc.add(RecordeVoicePlayEvent()); }, icon: Image.asset( 'voice_record_play'.assetPng, height: 30.h, width: 30.w, - ) - ), + )), Text( '录音', textAlign: TextAlign.center, style: TextStyle( - color: const Color(0xFF666666), - fontSize: 11.sp - ), + color: const Color(0xFF666666), fontSize: 11.sp), ), ], ), ), Offstage( - offstage: bloc.voiceRecordState == VoiceRecordState.voiceRecordUnkonw || bloc.xSCheckState != XSVoiceCheckState.unKnow, + offstage: + bloc.voiceRecordState == VoiceRecordState.voiceRecordUnkonw || + bloc.xSCheckState != XSVoiceCheckState.unKnow, child: Container( color: Colors.grey, - padding: EdgeInsets.symmetric( - vertical: 50.h, - horizontal: 50.w - ), + padding: EdgeInsets.symmetric(vertical: 50.h, horizontal: 50.w), child: Text( - bloc.voiceRecordState == VoiceRecordState.voiceRecording?'正在录音':'录音结束' - ), + bloc.voiceRecordState == VoiceRecordState.voiceRecording + ? '正在录音' + : '录音结束'), ), ), 10.verticalSpace, @@ -364,10 +358,7 @@ class _RepeatAfterContentPage extends StatelessWidget { Text( '按住录音', textAlign: TextAlign.center, - style: TextStyle( - color: const Color(0xFF333333), - fontSize: 16.sp - ), + style: TextStyle(color: const Color(0xFF333333), fontSize: 16.sp), ), ], ); diff --git a/packages/chivox_aiengine/LICENSE b/packages/chivox_aiengine/LICENSE new file mode 100644 index 0000000..ba75c69 --- /dev/null +++ b/packages/chivox_aiengine/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/packages/chivox_aiengine/README.md b/packages/chivox_aiengine/README.md new file mode 100644 index 0000000..5e38b9a --- /dev/null +++ b/packages/chivox_aiengine/README.md @@ -0,0 +1,22 @@ +# chivox_aiengine + +A new Flutter plugin project. + +## 应用层如何接入 + +1. 应用层通过本地插件的方式引用本插件: + ``` + # pubspec.yaml + dependencies: + chivox_aiengine: + path: *** # 此处填写本插件在磁盘上的存放路径 + ``` + +2. 拷贝驰声评测sdk的相关库文件至插件的相应目录中 + + ios + 应用层使用本插件之前先把iOS-SDK和通用SDK的静态库文件拷贝至本插件的`ios/Classes/Lib/`目录下:libCAIEngine.a, libaiengine.a + + android + 应用层使用之前先把android-SDK和通用SDK的相关库文件添加至本插件的android目录下的项目中:jar包和so库 + +## 插件接口文档 +请查看`lib/chivox_aiengine.dart`中的文档注释。 diff --git a/packages/chivox_aiengine/analysis_options.yaml b/packages/chivox_aiengine/analysis_options.yaml new file mode 100644 index 0000000..a5744c1 --- /dev/null +++ b/packages/chivox_aiengine/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/chivox_aiengine/android/build.gradle b/packages/chivox_aiengine/android/build.gradle new file mode 100644 index 0000000..aa91d70 --- /dev/null +++ b/packages/chivox_aiengine/android/build.gradle @@ -0,0 +1,54 @@ +group 'com.example.chivox_aiengine' +version '1.0' + +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.3.0' + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' + +android { + compileSdkVersion 31 + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + defaultConfig { + minSdkVersion 16 + } + + dependencies { + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.mockito:mockito-core:5.0.0' + } + + testOptions { + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } +} + +dependencies { + implementation files('libs/chivox_android_sdk_release.jar') +} diff --git a/packages/chivox_aiengine/android/libs/chivox_android_sdk_release.jar b/packages/chivox_aiengine/android/libs/chivox_android_sdk_release.jar new file mode 100644 index 0000000..9587a8e --- /dev/null +++ b/packages/chivox_aiengine/android/libs/chivox_android_sdk_release.jar diff --git a/packages/chivox_aiengine/android/settings.gradle b/packages/chivox_aiengine/android/settings.gradle new file mode 100644 index 0000000..ecab83e --- /dev/null +++ b/packages/chivox_aiengine/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'chivox_aiengine' diff --git a/packages/chivox_aiengine/android/src/main/AndroidManifest.xml b/packages/chivox_aiengine/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..7c8a4aa --- /dev/null +++ b/packages/chivox_aiengine/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/packages/chivox_aiengine/android/src/main/assets/vad.0.13.bin b/packages/chivox_aiengine/android/src/main/assets/vad.0.13.bin new file mode 100644 index 0000000..c86ec1d --- /dev/null +++ b/packages/chivox_aiengine/android/src/main/assets/vad.0.13.bin diff --git a/packages/chivox_aiengine/android/src/main/java/com/example/chivox_aiengine/ChivoxAienginePlugin.java b/packages/chivox_aiengine/android/src/main/java/com/example/chivox_aiengine/ChivoxAienginePlugin.java new file mode 100644 index 0000000..19a4867 --- /dev/null +++ b/packages/chivox_aiengine/android/src/main/java/com/example/chivox_aiengine/ChivoxAienginePlugin.java @@ -0,0 +1,466 @@ +package com.example.chivox_aiengine; + +import android.app.Activity; +import android.content.Context; + +import androidx.annotation.NonNull; + +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.embedding.engine.plugins.activity.ActivityAware; +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; +import io.flutter.embedding.engine.plugins.PluginRegistry; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.MethodChannel.MethodCallHandler; +import io.flutter.plugin.common.MethodChannel.Result; + +import com.chivox.aiengine.AudioSrc; +import com.chivox.aiengine.Engine; +import com.chivox.aiengine.EvalResult; +import com.chivox.aiengine.EvalResultListener; +import com.chivox.aiengine.RetValue; +import com.chivox.aiengine.ResTool; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** ChivoxAienginePlugin */ +public class ChivoxAienginePlugin implements FlutterPlugin, MethodCallHandler, ActivityAware { + /// The MethodChannel that will the communication between Flutter and native Android + /// + /// This local reference serves to register the plugin with the Flutter Engine and unregister it + /// when the Flutter Engine is detached from the Activity + private MethodChannel channel; + private MethodChannel callbackChannel; + private Map engines = new HashMap(); + private Context context; + private Activity activity; + FlutterPluginBinding binding; + + @Override + public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) { + channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "com.chivox.aiengine/call"); + channel.setMethodCallHandler(this); + context = flutterPluginBinding.getApplicationContext(); + + + callbackChannel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(),"com.chivox.aiengine/callback"); + binding = flutterPluginBinding; + } + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { + if (call.method.equals("getPlatformVersion")) { + result.success("Android " + android.os.Build.VERSION.RELEASE); + }else if(call.method.equals("aiengineNew")){ + handleAiengineNew(call, result); + } else if(call.method.equals("aiengineDelete")){ + handleAiengineDelete(call, result); + }else if(call.method.equals("aiengineStart")){ + handleAiengineStart(call, result); + }else if(call.method.equals("aiengineFeed")){ + handleAiengineFeed(call, result); + }else if(call.method.equals("aiengineStop")){ + handleAiengineStop(call, result); + }else if(call.method.equals("aiengineCancel")){ + handleAiengineCancel(call, result); + }else if(call.method.equals("getDeviceId")){ + handleGetDeviceId(call, result); + }else if(call.method.equals("getSerialNumber")){ + handleGetSerialNumber(call, result); + }else if(call.method.equals("clearSavedSerialNumber")){ + handleClearSavedSerialNumber(call, result); + }else if(call.method.equals("getProvision")){ + handleGetProvision(call, result); + }else if(call.method.equals("extractRes")){ + handleToolExtractRes(call, result); + }else if(call.method.equals("loadNativeCfg")){ + handleToolLoadNativeCfg(call, result); + }else { + result.notImplemented(); + } + } + + @Override + public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { + channel.setMethodCallHandler(null); + } + + public void handleAiengineNew(@NonNull MethodCall call, @NonNull Result result){ + ArrayList arguments = call.arguments(); + String cfg =(String) arguments.get(0); + JSONObject jcfg; + try { + jcfg = new JSONObject(cfg); + } catch (JSONException e) { + result.error("java_error", e.getMessage(), null); + return; + } + String engineId = UUID.randomUUID().toString(); + Lock lock = new ReentrantLock(); + Condition condition = lock.newCondition(); + final RetValue[] retErr = new RetValue[1]; + + + Engine.create(context, jcfg, new Engine.CreateCallback() { + @Override + public void onSuccess(Engine engine) { + engines.put(engineId, engine); + lock.lock(); + condition.signal(); + lock.unlock(); + + } + + @Override + public void onFail(RetValue retValue) { + retErr[0] = retValue; + lock.lock(); + condition.signal(); + lock.unlock(); + } + }); + lock.lock(); + try { + condition.await(); + } catch (InterruptedException e) { + result.error("java_error", e.getMessage(), null); + return; + }finally { + + lock.unlock(); + } + + if(engines.containsKey(engineId)){ + result.success(engineId); + }else{ + result.error(String.valueOf(retErr[0].errId), retErr[0].error,null); + } + } + + public void handleAiengineDelete(@NonNull MethodCall call, @NonNull Result result){ + ArrayList arguments = call.arguments(); + String eid =(String) arguments.get(0); + if(engines.containsKey(eid)) { + Engine e = engines.get(eid); + e.destroy(); + engines.remove(eid); + result.success(null); + return; + }else{ + result.success(null); + return; + } + } + + public static List makeAiEngineCallbackArgs(String callbackId, String type, EvalResult result){ + ArrayList retList = new ArrayList(); + retList.add(callbackId); + retList.add(type); + retList.add(result.isLast()); + retList.add(result.tokenId()!=null ? result.tokenId():""); + retList.add(result.text()); + retList.add(result.data() != null ? result.data():null); + retList.add(result.recFilePath()); + + return retList; + } + + public void handleAiengineStart(@NonNull MethodCall call, @NonNull Result result){ + ArrayList arguments = call.arguments(); + String eid =(String) arguments.get(0); + Map audioSrcMap = (Map)arguments.get(1); + String param = (String) arguments.get(2); + String callbackId = (String) arguments.get(3); + JSONObject audioSrcJson = new JSONObject(audioSrcMap); + Engine e = engines.get(eid); + StringBuilder tokenId = new StringBuilder(); + JSONObject paramJson; + AudioSrc audioSrc; + try { + paramJson = new JSONObject(param); + String srcType = audioSrcJson.getString("srcType"); + if(!srcType.equals("innerRecorder") && !srcType.equals("outerFeed")){ + result.error("-1", "invalid param audioSrc: 'srcType' not valid: "+srcType, null); + return; + } + if(srcType.equals("outerFeed")){ + audioSrc = new AudioSrc.OuterFeed(); + }else{ + AudioSrc.InnerRecorder innerRecorder = new AudioSrc.InnerRecorder(); + JSONObject innerRecorderParamJson = audioSrcJson.getJSONObject("innerRecorderParam"); + if(innerRecorderParamJson.has("duration")){ + innerRecorder.recordParam.duration = innerRecorderParamJson.getInt("duration"); + } + if(innerRecorderParamJson.has("channel")){ + innerRecorder.recordParam.channel = innerRecorderParamJson.getInt("channel"); + } + if(innerRecorderParamJson.has("sampleBytes")){ + innerRecorder.recordParam.sampleBytes = innerRecorderParamJson.getInt("sampleBytes"); + } + if(innerRecorderParamJson.has("sampleRate")){ + innerRecorder.recordParam.sampleRate = innerRecorderParamJson.getInt("sampleRate"); + } + if(innerRecorderParamJson.has("saveFile")){ + innerRecorder.recordParam.saveFile = new File(innerRecorderParamJson.getString("saveFile")); + } + + audioSrc = innerRecorder; + } + + } catch (JSONException ex) { + System.out.println(ex.getMessage()); + System.out.println(ex.toString()); + result.error("-1", ex.getMessage(), null); + return; + } + RetValue retValue = e.start(context, audioSrc, tokenId, paramJson, new EvalResultListener() { + @Override + public void onError(String tokenId, EvalResult result) { + System.out.println(result.type() + ", " + result.tokenId() + ", " + result.text()); + List args = makeAiEngineCallbackArgs(callbackId, "error", result); + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + callbackChannel.invokeMethod("aiengine_callback", args); + } + }); + } + + @Override + public void onEvalResult(String tokenId, EvalResult result) { + //System.out.println(result.type() + ", " + result.tokenId() + ", " + result.text()); + //System.out.println("isLast: " + result.isLast()); + List args = makeAiEngineCallbackArgs(callbackId, "evalResult", result); + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + callbackChannel.invokeMethod("aiengine_callback", args); + } + }); + } + + @Override + public void onBinResult(String tokenId, EvalResult result) { + System.out.println(result.type() + ", " + result.tokenId() + ", " + result.data().length); + List args = makeAiEngineCallbackArgs(callbackId, "binResult", result); + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + callbackChannel.invokeMethod("aiengine_callback", args); + } + }); + } + + @Override + public void onVad(String tokenId, EvalResult result) { + System.out.println(result.type() + ", " + result.tokenId() + ", " + result.text()); + List args = makeAiEngineCallbackArgs(callbackId, "vad", result); + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + callbackChannel.invokeMethod("aiengine_callback", args); + } + }); + } + + @Override + public void onSoundIntensity(String tokenId, EvalResult result) { + System.out.println(result.type() + ", " + result.tokenId() + ", " + result.text()); + List args = makeAiEngineCallbackArgs(callbackId, "soundIntensity", result); + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + callbackChannel.invokeMethod("aiengine_callback", args); + } + }); + } + + @Override + public void onOther(String tokenId, EvalResult result) { + System.out.println(result.type() + ", " + result.tokenId() + ", " + result.text()); + List args = makeAiEngineCallbackArgs(callbackId, "other", result); + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + callbackChannel.invokeMethod("aiengine_callback", args); + } + }); + } + } + ); + + if(retValue != null){ + if(retValue.errId == 0){ + result.success(tokenId.toString()); + }else{ + result.error(String.valueOf(retValue.errId),retValue.toString(), null); + } + }else{ + result.success(tokenId.toString()); + } + } + + public void handleAiengineFeed(@NonNull MethodCall call, @NonNull Result result){ + ArrayList arguments = call.arguments(); + String eid =(String) arguments.get(0); + byte[] data =(byte[]) arguments.get(1); + int length =(int) arguments.get(2); + + Engine e = engines.get(eid); + RetValue ret = e.feed(data, length); + if(ret != null){ + if(ret.errId == 0){ + result.success(null); + }else{ + result.error(String.valueOf(ret.errId),ret.toString(), null); + } + }else{ + result.success(null); + } + } + + public void handleAiengineStop(@NonNull MethodCall call, @NonNull Result result){ + ArrayList arguments = call.arguments(); + String eid =(String) arguments.get(0); + + Engine e = engines.get(eid); + RetValue ret = e.stop(); + + if(ret != null){ + if(ret.errId == 0){ + result.success(null); + }else{ + result.error(String.valueOf(ret.errId),ret.toString(), null); + } + }else{ + result.success(null); + } + } + + public void handleAiengineCancel(@NonNull MethodCall call, @NonNull Result result){ + ArrayList arguments = call.arguments(); + String eid =(String) arguments.get(0); + Engine e = engines.get(eid); + e.cancel(); + + result.success(null); + } + + public void handleGetDeviceId(@NonNull MethodCall call, @NonNull Result result){ + ArrayList arguments = call.arguments(); + + + String sid = Engine.getDeviceId(context); + result.success(sid); + } + + public void handleGetSerialNumber(@NonNull MethodCall call, @NonNull Result result){ + ArrayList arguments = call.arguments(); + String eid =(String) arguments.get(0); + Map input =(Map)arguments.get(1); + JSONObject inputJson = new JSONObject(input); + Engine e = engines.get(eid); + JSONObject serNum = Engine.getSerialNumber(context, e, inputJson); + + result.success(serNum.toString()); + } + + public void handleClearSavedSerialNumber(@NonNull MethodCall call, @NonNull Result result){ + ArrayList arguments = call.arguments(); + String appKey =(String) arguments.get(0); + boolean retB = Engine.clearSavedSerialNumber(context, appKey); + + result.success(retB); + } + + public void handleGetProvision(@NonNull MethodCall call, @NonNull Result result){ + ArrayList arguments = call.arguments(); + String eid =(String) arguments.get(0); + Map input =(Map)arguments.get(1); + JSONObject inputJson = new JSONObject(input); + Engine e = engines.get(eid); + JSONObject provision = Engine.getProvision(context, e, inputJson); + + result.success(provision.toString()); + } + + public void handleToolExtractRes(@NonNull MethodCall call, @NonNull Result result){ + ArrayList arguments = call.arguments(); + String assetPrefix = (String) arguments.get(0); + List assetNames = (List) arguments.get(1); + String targetRoot = (String) arguments.get(2); + String callbackId = (String) arguments.get(3); + File targetRootFile = new File(targetRoot); + List keys = new ArrayList(); + + for(String assetname : assetNames){ + String key = binding.getFlutterAssets().getAssetFilePathByName(assetPrefix+"/"+assetname); + System.out.println(key); + keys.add(key); + } + String ret = ResTool.extract(context.getAssets(),keys.toArray(new String[0]), targetRootFile, new ResTool.ExtractHint(){ + + @Override + public void onProgress(float v) { + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + ArrayList args = new ArrayList(); + args.add(callbackId); + args.add(v); + callbackChannel.invokeMethod("aiengine_callback", args); + } + }); + } + }); + + if(ret != null){ + result.error("-1", ret, null); + }else{ + result.success(null); + } + } + + public void handleToolLoadNativeCfg(@NonNull MethodCall call, @NonNull Result result){ + ArrayList arguments = call.arguments(); + String resRoot =(String) arguments.get(0); + + List resNames = (List) arguments.get(1); + File resRootFile = new File(resRoot); + + JSONObject retJson = ResTool.loadNativeCfg(resRootFile, resNames.toArray(new String[0])); + + result.success(retJson.toString()); + } + + @Override + public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) { + activity = binding.getActivity(); + } + + @Override + public void onDetachedFromActivityForConfigChanges() { + + } + + @Override + public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) { + activity = binding.getActivity(); + } + + @Override + public void onDetachedFromActivity() { + + } +} diff --git a/packages/chivox_aiengine/android/src/main/jniLibs/arm64-v8a/libaiengine.so b/packages/chivox_aiengine/android/src/main/jniLibs/arm64-v8a/libaiengine.so new file mode 100644 index 0000000..f962345 --- /dev/null +++ b/packages/chivox_aiengine/android/src/main/jniLibs/arm64-v8a/libaiengine.so diff --git a/packages/chivox_aiengine/android/src/main/jniLibs/armeabi-v7a/libaiengine.so b/packages/chivox_aiengine/android/src/main/jniLibs/armeabi-v7a/libaiengine.so new file mode 100644 index 0000000..4175c32 --- /dev/null +++ b/packages/chivox_aiengine/android/src/main/jniLibs/armeabi-v7a/libaiengine.so diff --git a/packages/chivox_aiengine/android/src/main/jniLibs/x86_64/libaiengine.so b/packages/chivox_aiengine/android/src/main/jniLibs/x86_64/libaiengine.so new file mode 100644 index 0000000..964b5a0 --- /dev/null +++ b/packages/chivox_aiengine/android/src/main/jniLibs/x86_64/libaiengine.so diff --git a/packages/chivox_aiengine/ios/Classes/CAIEngine/CAIEngine.h b/packages/chivox_aiengine/ios/Classes/CAIEngine/CAIEngine.h new file mode 100644 index 0000000..13550a1 --- /dev/null +++ b/packages/chivox_aiengine/ios/Classes/CAIEngine/CAIEngine.h @@ -0,0 +1,25 @@ +// +// CAIEngine.h +// CAIEngine +// +// Created by chivox on 2019/10/21. +// Copyright © 2019 chivox. All rights reserved. +// + +/* + * 导出头文件 + */ + +#import "ChivoxAISdkInfo.h" +#import "ChivoxAIRetValue.h" +#import "ChivoxAIEngine.h" +#import "ChivoxAIEvalResult.h" +#import "ChivoxAIRecorderNotify.h" +#import "ChivoxAIResTool.h" +#import "ChivoxAILogLevel.h" +#import "ChivoxAIAudioPlayer.h" +#import "ChivoxAIRecordParam.h" +#import "ChivoxAIAudioSrc.h" +#import "ChivoxAIGlobalCfg.h" + + diff --git a/packages/chivox_aiengine/ios/Classes/CAIEngine/ChivoxAIAudioPlayer.h b/packages/chivox_aiengine/ios/Classes/CAIEngine/ChivoxAIAudioPlayer.h new file mode 100644 index 0000000..ff6b65f --- /dev/null +++ b/packages/chivox_aiengine/ios/Classes/CAIEngine/ChivoxAIAudioPlayer.h @@ -0,0 +1,27 @@ +// +// ChivoxAIAudioPlayer.h +// CAIEngine +// +// Created by chivox on 2019/10/21. +// Copyright © 2019 chivox. All rights reserved. +// + +#import +#import + +@class ChivoxAIAudioPlayer; + +@interface ChivoxAIAudioPlayerListener : NSObject +@property (nonatomic, strong) void (^onStarted)(ChivoxAIAudioPlayer *ap); +@property (nonatomic, strong) void (^onStopped)(ChivoxAIAudioPlayer *ap); +@property (nonatomic, strong) void (^onError)(ChivoxAIAudioPlayer *ap, NSString *err); +@end + +@interface ChivoxAIAudioPlayer : NSObject + ++ (ChivoxAIAudioPlayer *)sharedInstance; +- (void)setListener:(ChivoxAIAudioPlayerListener *)_event; +- (void)playFile:(NSString *)path; +- (void)cancel; + +@end diff --git a/packages/chivox_aiengine/ios/Classes/CAIEngine/ChivoxAIAudioSrc.h b/packages/chivox_aiengine/ios/Classes/CAIEngine/ChivoxAIAudioSrc.h new file mode 100644 index 0000000..3b214b7 --- /dev/null +++ b/packages/chivox_aiengine/ios/Classes/CAIEngine/ChivoxAIAudioSrc.h @@ -0,0 +1,26 @@ +// +// ChivoxAIAudioSrc.h +// CAIEngine +// +// Created by sq-ios92 on 2020/3/26. +// Copyright © 2020年 chivox. All rights reserved. +// + +#import +#import "ChivoxAIRecordParam.h" +@class ChivoxAIRecordParam; + +@interface ChivoxAIAudioSrc : NSObject +@end + +@interface ChivoxAIOuterFeed : ChivoxAIAudioSrc + +@end + +@interface ChivoxAIInnerRecorder : ChivoxAIAudioSrc +/** + *录音参数可以设置,具体请参考ChivoxAIRecordParam类。 + */ +@property (nonnull,nonatomic,strong)ChivoxAIRecordParam *recordParam; + +@end diff --git a/packages/chivox_aiengine/ios/Classes/CAIEngine/ChivoxAIEngine.h b/packages/chivox_aiengine/ios/Classes/CAIEngine/ChivoxAIEngine.h new file mode 100644 index 0000000..bc744cb --- /dev/null +++ b/packages/chivox_aiengine/ios/Classes/CAIEngine/ChivoxAIEngine.h @@ -0,0 +1,137 @@ +// +// ChivoxAIEngine.h +// CAIEngine +// +// Created by chivox on 2019/10/21. +// Copyright © 2019 chivox. All rights reserved. +// + +#import + +@class ChivoxAIEngine; +@class ChivoxAIRetValue; +@class ChivoxAIEval; +@class ChivoxAISdkInfo; +@class ChivoxAIEvalResult; +@class ChivoxAIRetValue; +@class ChivoxAIEvalParam; +@class ChivoxAIEvalResultListener; +@class ChivoxAIAudioSrc; + + +NS_ASSUME_NONNULL_BEGIN + +/** + * 引擎创建成功回调 + * @param engine 已创建的引擎对象 + */ +typedef void (^ChivoxAIEngineCreateSuccess)(ChivoxAIEngine *engine); +/** + * 引擎创建失败回调 + * @param err 失败信息 + */ +typedef void (^ChivoxAIEngineCreateFail)(ChivoxAIRetValue *err); + +/** + * 引擎创建回调类 + */ +@interface ChivoxAIEngineCreateCallback : NSObject +/** + * 创建回调对象 + * @param success 成功回调 + * @param fail 失败回调 + */ ++ (instancetype)onSuccess:(ChivoxAIEngineCreateSuccess)success onFail:(ChivoxAIEngineCreateFail)fail; +- (instancetype)init NS_UNAVAILABLE; // 禁止外部调用init +@end + + +/** + * 评测引擎类 + */ +@interface ChivoxAIEngine : NSObject +/** + * 二次封装日志 + */ +@property (nonatomic, strong, nullable) NSString *logFile; +/** + * 创建引擎(异步的) + * @param cfg 引擎配置 + * @param cb 回调 + */ ++ (void)create:(NSMutableDictionary *)cfg cb:(ChivoxAIEngineCreateCallback *)cb; +- (instancetype)init NS_UNAVAILABLE; // 禁止外部调用init + +- (void)setWifiStatus:(NSString *)status; + +/** + * 评测开始 + * @param param 评测参数 + * @return 错误信息 + */ +- (ChivoxAIRetValue *)start:(ChivoxAIAudioSrc *)audioSrc tokenId:(NSMutableString *)tokenId param:(NSMutableDictionary *)param listener:(ChivoxAIEvalResultListener *)listener; + +/** + * 输入音频数据(内部录音模式无需调用本方法) + * @param bytes 数据缓冲区 + * @param length 长度 + * @return 错误信息 + */ +- (ChivoxAIRetValue *)feed:(const void *)bytes length:(int)length; + +/** + * 评测结束 + * @return 错误信息 + */ +- (ChivoxAIRetValue *)stop; + +/** + * 取消评测 + */ +- (void)cancel; + +/** + * 销毁引擎 + */ +- (void)destory; + +/** + * 获取设备ID + */ ++ (nullable NSString *)getDeviceId; +/** + * 设备激活, 方法内部会把序列号保存在[NSUserDefaults standardUserDefaults]中, 下次调用直接返回已保存的值 + * @param input JSON { "appKey": "", "secretKey": ""} + * @return JSON {"error": "", "serialNumber": ""} + */ ++ (nullable NSDictionary *)getSerialNumber:(NSDictionary *)input; +/** + * 删除已保存的序列号 + * @param appKey 驰声AppKey + */ ++ (BOOL)clearSavedSerialNumber:(NSString *)appKey; +/** + * 二次授权(某些AppKey支持二次授权) + * @param input JSON {"appKey": "", "secretKey": ""} + * @return JSON {"error": "", "serialNumber": "", "provision": ""} provision字段为base64格式的证书文件 + */ ++ (nullable NSDictionary *)getProvision:(NSDictionary *)input; +/** + * 获取sdk信息 + */ ++ (ChivoxAISdkInfo *)sdkInfo; +@end + +/** + * 评测结果监听类 + */ +@interface ChivoxAIEvalResultListener : NSObject +@property (nonatomic, strong) void (^onEvalResult)(NSString *tokenId, ChivoxAIEvalResult *result); +@property (nonatomic, strong) void (^onBinResult)(NSString *tokenId, ChivoxAIEvalResult *result); +@property (nonatomic, strong) void (^onError)(NSString *tokenId, ChivoxAIEvalResult *result); +@property (nonatomic, strong) void (^onVad)(NSString *tokenId, ChivoxAIEvalResult *result); +@property (nonatomic, strong) void (^onSoundIntensity)(NSString *tokenId, ChivoxAIEvalResult *result); +@property (nonatomic, strong) void (^onOther)(NSString *tokenId, ChivoxAIEvalResult *result); +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/chivox_aiengine/ios/Classes/CAIEngine/ChivoxAIEvalResult.h b/packages/chivox_aiengine/ios/Classes/CAIEngine/ChivoxAIEvalResult.h new file mode 100644 index 0000000..a2ff60c --- /dev/null +++ b/packages/chivox_aiengine/ios/Classes/CAIEngine/ChivoxAIEvalResult.h @@ -0,0 +1,31 @@ +// +// ChivoxAIEvalResult.h +// CAIEngine +// +// Created by chivox on 2019/10/21. +// Copyright © 2019 chivox. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef enum { + kChivoxAIResultTypeUnknown = 0, + kChivoxAIResultTypeError = 1, + kChivoxAIResultTypeResult = 2, + kChivoxAIResultTypeBin = 3, + kChivoxAIResultTypeVad = 4, + kChivoxAIResultTypeSoundIntensity = 5, +} ChivoxAIEvalResultType; + +@interface ChivoxAIEvalResult : NSObject +@property (nonatomic, assign) BOOL isLast; +@property (nonatomic, assign) ChivoxAIEvalResultType type; +@property (nonatomic, strong, nullable) NSString *tokenId; +@property (nonatomic, strong, nullable) NSString *text; +@property (nonatomic, strong, nullable) NSData *data; +@property (nonatomic, strong, nullable) NSString *recFilePath; +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/chivox_aiengine/ios/Classes/CAIEngine/ChivoxAIGlobalCfg.h b/packages/chivox_aiengine/ios/Classes/CAIEngine/ChivoxAIGlobalCfg.h new file mode 100644 index 0000000..ba7f2b7 --- /dev/null +++ b/packages/chivox_aiengine/ios/Classes/CAIEngine/ChivoxAIGlobalCfg.h @@ -0,0 +1,21 @@ +// +// ChivoxAIGlobalCfg.h +// CAIEngine +// +// Created by sq-ios92 on 2020/12/3. +// Copyright © 2020年 chivox. All rights reserved. +// + +#import + +@interface ChivoxAIGlobalCfg : NSObject + ++ (NSString *)getUserId; + ++ (void)setUserId:(NSString *)userId; + ++ (BOOL)isLogUpEnable; + ++ (void)setLogUpEnable:(BOOL)enable; + +@end diff --git a/packages/chivox_aiengine/ios/Classes/CAIEngine/ChivoxAILogLevel.h b/packages/chivox_aiengine/ios/Classes/CAIEngine/ChivoxAILogLevel.h new file mode 100644 index 0000000..9ffddfd --- /dev/null +++ b/packages/chivox_aiengine/ios/Classes/CAIEngine/ChivoxAILogLevel.h @@ -0,0 +1,20 @@ +// +// ChivoxAILogLevel.h +// CAIEngine +// +// Created by chivox on 2019/11/20. +// Copyright © 2019 chivox. All rights reserved. +// + +#ifndef ChivoxAILogLevel_h +#define ChivoxAILogLevel_h + +enum { + kChivoxAILogLevelDebug = 0, // 调试信息 + kChivoxAILogLevelInfo = 1, // 普通信息 + kChivoxAILogLevelNoti = 2, // 注意信息 + kChivoxAILogLevelWarn = 3, // 警告信息 + kChivoxAILogLevelError = 4, // 错误信息 +}; + +#endif /* ChivoxAILogLevel_h */ diff --git a/packages/chivox_aiengine/ios/Classes/CAIEngine/ChivoxAIRecordParam.h b/packages/chivox_aiengine/ios/Classes/CAIEngine/ChivoxAIRecordParam.h new file mode 100644 index 0000000..d8ddb0a --- /dev/null +++ b/packages/chivox_aiengine/ios/Classes/CAIEngine/ChivoxAIRecordParam.h @@ -0,0 +1,45 @@ +// +// ChivoxAIRecordParam.h +// CAIEngine +// +// Created by chivox on 2019/10/30. +// Copyright © 2019 chivox. All rights reserved. +// + +#import + +@interface ChivoxAIRecordParam : NSObject + +/** + * 录音时长, 毫秒. 如果值小于或等于0, 表示不限录音时长. + * 缺省值: 0 + */ +@property (nonatomic, assign) int duration; + +/** + * 通道数 + * 有效值: [1] + * 缺省值: 1 + */ +@property (nonatomic, assign) int channel; + +/** + * 采样字节数 + * 有效值: [1, 2] + * 缺省值: 2 + */ +@property (nonatomic, assign) int sampleBytes; + +/** + * 采样率 + * 有效值: [8000, 48000] + * 缺省值: 16000 + */ +@property (nonatomic, assign) int sampleRate; + +/** + * 录音机保存文件的路径 + */ +@property (nonatomic, strong, nullable) NSString *saveFile; + +@end diff --git a/packages/chivox_aiengine/ios/Classes/CAIEngine/ChivoxAIRecorderNotify.h b/packages/chivox_aiengine/ios/Classes/CAIEngine/ChivoxAIRecorderNotify.h new file mode 100644 index 0000000..f418416 --- /dev/null +++ b/packages/chivox_aiengine/ios/Classes/CAIEngine/ChivoxAIRecorderNotify.h @@ -0,0 +1,18 @@ +// +// ChivoxAIRecorderNotify.h +// CAIEngine +// +// Created by chivox on 2019/10/21. +// Copyright © 2019 chivox. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface ChivoxAIRecorderNotify : NSObject +@property (nonatomic, strong, nullable) void (^onRecordStart)(void); +@property (nonatomic, strong, nullable) void (^onRecordStop)(void); ++ (ChivoxAIRecorderNotify *)sharedInstance; +@end +NS_ASSUME_NONNULL_END diff --git a/packages/chivox_aiengine/ios/Classes/CAIEngine/ChivoxAIResTool.h b/packages/chivox_aiengine/ios/Classes/CAIEngine/ChivoxAIResTool.h new file mode 100644 index 0000000..565502f --- /dev/null +++ b/packages/chivox_aiengine/ios/Classes/CAIEngine/ChivoxAIResTool.h @@ -0,0 +1,31 @@ +// +// ChivoxAIResTool.h +// CAIEngine +// +// Created by chivox on 2019/10/21. +// Copyright © 2019 chivox. All rights reserved. +// + +#import + + +@interface ChivoxAIResTool : NSObject +/** + * @param progress 进度反馈 + */ +@property (nonatomic, strong) void (^onProgress)(float i); + +/** + * 解压评测资源. 本方法会阻塞,所以请在后台线程调用本方法. + * @param assetMng 资源源路径 + * @param assetNames 资源文件名列表, 例如: {"en.word.score.zip", "en.sent.score.zip", "cn.word.score.zip", ...}, 按需填写. + * 资源文件名应包含".zip"后缀, 解压后将在目标目录下产生一个文件夹, 该文件夹的名称为资源文件名去掉".zip"后缀. + * @param resRoot 目标目录. + * 如果目标目录中已存在相同资源(md5sum相同), 则不会重复执行解压操作, 如果目标目录下的资源md5sum不相同, 则会覆盖目标目录下的资源. + * @return 是否解压成功. 返回null表示成功, 否则表示失败以及失败原因. + */ +- (NSString *)extract:(NSString *)assetMng assets:(NSMutableArray *)assetNames target:(NSString *)resRoot; + +- (NSString *)getVadResPath:(NSString *)resRoot :(NSString *)resName; +- (NSMutableDictionary *)loadNativeCfg:(NSString *)resRoot :(NSMutableArray *)resNames; +@end diff --git a/packages/chivox_aiengine/ios/Classes/CAIEngine/ChivoxAIRetValue.h b/packages/chivox_aiengine/ios/Classes/CAIEngine/ChivoxAIRetValue.h new file mode 100644 index 0000000..762dd09 --- /dev/null +++ b/packages/chivox_aiengine/ios/Classes/CAIEngine/ChivoxAIRetValue.h @@ -0,0 +1,29 @@ +// +// ChivoxAIRetValue.h +// CAIEngine +// +// Created by chivox on 2019/11/13. +// Copyright © 2019 chivox. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * 返回信息包装类 + */ +@interface ChivoxAIRetValue : NSObject +/** + * 错误码 + */ +- (int)errId; +/** + * 错误信息 + */ +- (NSString *)error; +@end + +NS_ASSUME_NONNULL_END + + diff --git a/packages/chivox_aiengine/ios/Classes/CAIEngine/ChivoxAISdkInfo.h b/packages/chivox_aiengine/ios/Classes/CAIEngine/ChivoxAISdkInfo.h new file mode 100644 index 0000000..52607fd --- /dev/null +++ b/packages/chivox_aiengine/ios/Classes/CAIEngine/ChivoxAISdkInfo.h @@ -0,0 +1,25 @@ +// +// ChivoxAISdkInfo.h +// CAIEngine +// +// Created by chivox on 2019/10/21. +// Copyright © 2019 chivox. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface ChivoxAISdkInfo : NSObject +- (instancetype)init NS_UNAVAILABLE;// 禁止外部调用init +- (int)versionMajor; +- (int)versionMinor; +- (int)versionPatch; +- (int)versionTweak; +- (NSString *)versionBuild; +- (NSString *)version; +- (NSString *)commonSdkVersion; +- (NSString *)commonSdkModules; +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/chivox_aiengine/ios/Classes/ChivoxAienginePlugin.h b/packages/chivox_aiengine/ios/Classes/ChivoxAienginePlugin.h new file mode 100644 index 0000000..a3dd94b --- /dev/null +++ b/packages/chivox_aiengine/ios/Classes/ChivoxAienginePlugin.h @@ -0,0 +1,4 @@ +#import + +@interface ChivoxAienginePlugin : NSObject +@end diff --git a/packages/chivox_aiengine/ios/Classes/ChivoxAienginePlugin.m b/packages/chivox_aiengine/ios/Classes/ChivoxAienginePlugin.m new file mode 100644 index 0000000..6d8098f --- /dev/null +++ b/packages/chivox_aiengine/ios/Classes/ChivoxAienginePlugin.m @@ -0,0 +1,612 @@ +#import "ChivoxAienginePlugin.h" +#include "CAIEngine/CAIEngine.h" + +// 经过实际测试,有以下结论: +// 1. dart和oc代码运行在不同线程,oc代码中阻塞并不会导致dart阻塞 +// 2. 每次dart调用oc代码,都会派发到oc主线程调用,并且是串行方式。 + +@implementation ChivoxAienginePlugin +{ + NSMutableDictionary *engineDic; + FlutterMethodChannel *callbackChannel; + NSObject *registrar; + +} + ++ (void)registerWithRegistrar:(NSObject*)registrar { + FlutterMethodChannel* channel = [FlutterMethodChannel + methodChannelWithName:@"com.chivox.aiengine/call" + binaryMessenger:[registrar messenger]]; + ChivoxAienginePlugin* instance = [[ChivoxAienginePlugin alloc] initWithRegistrar:registrar]; + [registrar addMethodCallDelegate:instance channel:channel]; +} + +- (instancetype) init { + if (self = [super init]) { + self->engineDic = [NSMutableDictionary dictionary]; + } + return self; +} + +- (instancetype) initWithRegistrar:(NSObject*)registrar { + if (self = [self init]) { + self->callbackChannel = [FlutterMethodChannel methodChannelWithName:@"com.chivox.aiengine/callback" binaryMessenger:[registrar messenger]]; + self->registrar = registrar; + } + return self; +} + +- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { + if ([@"getPlatformVersion" isEqualToString:call.method]) { + result([@"iOS " stringByAppendingString:[[UIDevice currentDevice] systemVersion]]); + } + else if ([@"aiengineNew" isEqualToString:call.method]) { + [self handle_aiengineNew:call result:result]; + } + else if ([@"aiengineDelete" isEqualToString:call.method]) { + [self handle_aiengineDelete:call result:result]; + } + else if ([@"aiengineStart" isEqualToString:call.method]) { + [self handle_aiengineStart:call result:result]; + } + else if ([@"aiengineFeed" isEqualToString:call.method]) { + [self handle_aiengineFeed:call result:result]; + } + else if ([@"aiengineStop" isEqualToString:call.method]) { + [self handle_aiengineStop:call result:result]; + } + else if ([@"aiengineCancel" isEqualToString:call.method]) { + [self handle_aiengineCancel:call result:result]; + } + else if ([@"getDeviceId" isEqualToString:call.method]) { + [self handle_getDeviceId:call result:result]; + } + else if ([@"getSerialNumber" isEqualToString:call.method]) { + [self handle_getSerialNumber:call result:result]; + } + else if ([@"clearSavedSerialNumber" isEqualToString:call.method]) { + [self handle_clearSavedSerialNumber:call result:result]; + } + else if ([@"getProvision" isEqualToString:call.method]) { + [self handle_getProvision:call result:result]; + } + else if ([@"extractRes" isEqualToString:call.method]) { + [self handle_toolExtractRes:call result:result]; + } + else if ([@"loadNativeCfg" isEqualToString:call.method]) { + [self handle_toolLoadNativeCfg:call result:result]; + } + else { + result(FlutterMethodNotImplemented); + } +} + +static NSString * _GenerateEngineId() { + static dispatch_once_t onceToken; + static NSLock *_lock = nil; + dispatch_once(&onceToken, ^{ + _lock = [[NSLock alloc] init]; + }); + + int cnt = 0; + @synchronized (_lock) { + static int _cnt = 0; + cnt = _cnt; + _cnt++; + if (_cnt > 65535) { + _cnt = 0; + } + } + + int64_t timestamp = [[NSDate date] timeIntervalSince1970]; + NSString *engineId = [NSString stringWithFormat:@"%llx-%x", timestamp, cnt]; + return engineId; +} + +#define _ParseJsonString(str, err) \ +[NSJSONSerialization JSONObjectWithData:[str dataUsingEncoding:NSUTF8StringEncoding] options:NSJSONReadingMutableContainers error:err] + +#define _CondSignal(cond) do {\ +[cond lock]; \ +[cond signal]; \ +[cond unlock]; \ +}while(0) + +#define _CondWait(cond) do {\ +[cond lock]; \ +[cond wait]; \ +[cond unlock]; \ +}while(0) + +/** + handle_aiengineNew + @param call + call.arguments[0]: cfg string; + @param result engineId / FlutterError + */ +- (void)handle_aiengineNew:(FlutterMethodCall*)call result:(FlutterResult)result { + NSString *strCfg = (NSString *) call.arguments[0]; + + // 检查参数 + NSError *jerr = nil; + id cfg = _ParseJsonString(strCfg, &jerr); + if (jerr != nil) { + int eid = -1; + id emsg = @"cfg is not valid json"; + result([FlutterError errorWithCode:[NSString stringWithFormat:@"%d", eid] message:emsg details:nil]); + return; + } + if (![cfg isKindOfClass:[NSMutableDictionary class]]) { + int eid = -1; + id emsg = @"cfg is not in jsonobject type"; + result([FlutterError errorWithCode:[NSString stringWithFormat:@"%d", eid] message:emsg details:nil]); + return; + } + + NSCondition *cond = [[NSCondition alloc] init]; + __block NSString *engineId = @""; + __block int eid = 0; + __block NSString *emsg = @""; + + // 创建引擎 + [ChivoxAIEngine create:cfg cb:[ChivoxAIEngineCreateCallback onSuccess:^(ChivoxAIEngine * _Nonnull engine) { + engineId = _GenerateEngineId(); + NSLog(@"ChivoxAIEngine create success: engineId=%@, engine=%p", engineId, engine); + if ([engineId length] == 0) { + if (eid == 0) eid = -1; + result([FlutterError errorWithCode:[NSString stringWithFormat:@"%d", eid] message:@"generate engineid fail" details:nil]); + return; + } + + [self->engineDic setObject:engine forKey:engineId]; + //_CondSignal(cond); + result(engineId); + + } onFail:^(ChivoxAIRetValue * _Nonnull err) { + NSLog(@"ChivoxAIEngine create fail: %@", err); + eid = err.errId; + emsg = err.error; + if (eid == 0) eid = -1; + //_CondSignal(cond); + result([FlutterError errorWithCode:[NSString stringWithFormat:@"%d", eid] message:emsg details:nil]); + }]]; + + //_CondWait(cond); + + //if ([engineId length] == 0) { + // if (eid == 0) eid = -1; + // result([FlutterError errorWithCode:[NSString stringWithFormat:@"%d", eid] message:emsg details:nil]); + // return; + //} + + //result(engineId); +} + +/** + handle_aiengineDelete + @param call + call.arguments[0]: engineId; + @param result void + */ +- (void)handle_aiengineDelete:(FlutterMethodCall*)call result:(FlutterResult)result { + NSString *engineId = (NSString *) call.arguments[0]; + ChivoxAIEngine *engine = [self->engineDic objectForKey:engineId]; + if (engine != nil) { + [self->engineDic removeObjectForKey:engineId]; + [engine destory]; + } + result(nil); +} + +static id _CheckMakeAudioSrc(NSDictionary *mapAudioSrc, FlutterError **err) { + *err = nil; + + id srcType = [mapAudioSrc objectForKey:@"srcType"]; + if (srcType == nil || ![srcType isKindOfClass:[NSString class]]) { + int eid = -1; + id emsg = @"invalid param audioSrc: 'srcType' required, must be String"; + *err = [FlutterError errorWithCode:[NSString stringWithFormat:@"%d", eid] message:emsg details:nil]; + return nil; + } + + if (![srcType isEqualToString:@"innerRecorder"] && ![srcType isEqualToString:@"outerFeed"]) { + int eid = -1; + id emsg = [NSString stringWithFormat:@"invalid param audioSrc: 'srcType' not valid: %@", srcType]; + *err = [FlutterError errorWithCode:[NSString stringWithFormat:@"%d", eid] message:emsg details:nil]; + return nil; + } + + if ([srcType isEqualToString:@"outerFeed"]) { + return [[ChivoxAIOuterFeed alloc] init]; + } + // else innerRecorder + + ChivoxAIInnerRecorder *inner = [[ChivoxAIInnerRecorder alloc] init]; + + id innerParam = [mapAudioSrc objectForKey:@"innerRecorderParam"]; + if (innerParam == nil) { + return inner; + } + + if (![innerParam isKindOfClass:[NSDictionary class]]) { + int eid = -1; + id emsg = @"invalid param audioSrc: 'innerRecorderParam' must be Map or null"; + *err = [FlutterError errorWithCode:[NSString stringWithFormat:@"%d", eid] message:emsg details:nil]; + return nil; + } + + id duration = [innerParam objectForKey:@"duration"]; + id channel = [innerParam objectForKey:@"channel"]; + id sampleBytes = [innerParam objectForKey:@"sampleBytes"]; + id sampleRate = [innerParam objectForKey:@"sampleRate"]; + id saveFile = [innerParam objectForKey:@"saveFile"]; + + if (duration != nil && ![duration isKindOfClass:[NSNumber class]]) { + int eid = -1; + id emsg = @"invalid param audioSrc: 'innerRecorderParam.duration' must be number or null"; + *err = [FlutterError errorWithCode:[NSString stringWithFormat:@"%d", eid] message:emsg details:nil]; + return nil; + } + if (channel != nil && ![channel isKindOfClass:[NSNumber class]]) { + int eid = -1; + id emsg = @"invalid param audioSrc: 'innerRecorderParam.channel' must be number or null"; + *err = [FlutterError errorWithCode:[NSString stringWithFormat:@"%d", eid] message:emsg details:nil]; + return nil; + } + if (sampleBytes != nil && ![sampleBytes isKindOfClass:[NSNumber class]]) { + int eid = -1; + id emsg = @"invalid param audioSrc: 'innerRecorderParam.sampleBytes' must be number or nul"; + *err = [FlutterError errorWithCode:[NSString stringWithFormat:@"%d", eid] message:emsg details:nil]; + return nil; + } + if (sampleRate != nil && ![sampleRate isKindOfClass:[NSNumber class]]) { + int eid = -1; + id emsg = @"invalid param audioSrc: 'innerRecorderParam.sampleRate' must be number or null"; + *err = [FlutterError errorWithCode:[NSString stringWithFormat:@"%d", eid] message:emsg details:nil]; + return nil; + } + if (saveFile != nil && ![saveFile isKindOfClass:[NSString class]]) { + int eid = -1; + id emsg = @"invalid param audioSrc: 'innerRecorderParam.saveFile' must be String or null"; + *err = [FlutterError errorWithCode:[NSString stringWithFormat:@"%d", eid] message:emsg details:nil]; + return nil; + } + + if (duration != nil) { + inner.recordParam.duration = [duration intValue]; + } + if (channel != nil) { + inner.recordParam.channel = [channel intValue]; + } + if (sampleBytes != nil) { + inner.recordParam.sampleBytes = [sampleBytes intValue]; + } + if (sampleRate != nil) { + inner.recordParam.sampleRate = [sampleRate intValue]; + } + if (saveFile != nil) { + inner.recordParam.saveFile = saveFile; + } + + return inner; +} + +static id _MakeAiengineCallbackArgs(NSString *callbackId, NSString *type, ChivoxAIEvalResult *result) { + return @[ + callbackId, + type, + @(result.isLast), + result.tokenId ? result.tokenId : @"", + result.text ? result.text : [NSNull null], + result.data ? [FlutterStandardTypedData typedDataWithBytes:result.data] : [NSNull null], + result.recFilePath ? result.recFilePath : [NSNull null], + ]; +}; + +/** + handle_aiengineStart + @param call + call.arguments[0]: engineId; + call.arguments[1]: audioSrc Map; + call.arguments[2]: param string; + @param result tokenId / FlutterError + */ +- (void)handle_aiengineStart:(FlutterMethodCall*)call result:(FlutterResult)result { + NSString *engineId = (NSString *) call.arguments[0]; + NSDictionary *mapAudioSrc = (NSDictionary *) call.arguments[1]; + NSString *strParam = (NSString *) call.arguments[2]; + NSString *callbackId = (NSString *) call.arguments[3]; + + FlutterError *flerr = nil; + id audioSrc = _CheckMakeAudioSrc(mapAudioSrc, &flerr); + if (flerr != nil) { + result(flerr); + return; + } + + NSError *jerr = nil; + id param = _ParseJsonString(strParam, &jerr); + if (jerr != nil) { + int eid = -1; + id emsg = @"param is not valid json"; + result([FlutterError errorWithCode:[NSString stringWithFormat:@"%d", eid] message:emsg details:nil]); + return; + } + if (![param isKindOfClass:[NSMutableDictionary class]]) { + int eid = -1; + id emsg = @"param is not in jsonobject type"; + result([FlutterError errorWithCode:[NSString stringWithFormat:@"%d", eid] message:emsg details:nil]); + return; + } + + ChivoxAIEngine *engine = [self->engineDic objectForKey:engineId]; + if (engine == nil) { + int eid = -1; + id emsg = @"the engineId doesn't represent any engine"; + result([FlutterError errorWithCode:[NSString stringWithFormat:@"%d", eid] message:emsg details:nil]); + return; + } + + NSMutableString *tokenId = [NSMutableString string]; + + ChivoxAIEvalResultListener *listener = [[ChivoxAIEvalResultListener alloc] init]; + listener.onEvalResult = ^(NSString * _Nonnull tokenId, ChivoxAIEvalResult * _Nonnull result) { + id args = _MakeAiengineCallbackArgs(callbackId, @"evalResult", result); + [self->callbackChannel invokeMethod:@"aiengine_callback" arguments:args]; + }; + listener.onBinResult = ^(NSString * _Nonnull tokenId, ChivoxAIEvalResult * _Nonnull result) { + id args = _MakeAiengineCallbackArgs(callbackId, @"binResult", result); + [self->callbackChannel invokeMethod:@"aiengine_callback" arguments:args]; + }; + listener.onError = ^(NSString * _Nonnull tokenId, ChivoxAIEvalResult * _Nonnull result) { + id args = _MakeAiengineCallbackArgs(callbackId, @"error", result); + [self->callbackChannel invokeMethod:@"aiengine_callback" arguments:args]; + }; + listener.onVad = ^(NSString * _Nonnull tokenId, ChivoxAIEvalResult * _Nonnull result) { + id args = _MakeAiengineCallbackArgs(callbackId, @"vad", result); + [self->callbackChannel invokeMethod:@"aiengine_callback" arguments:args]; + }; + listener.onSoundIntensity = ^(NSString * _Nonnull tokenId, ChivoxAIEvalResult * _Nonnull result) { + id args = _MakeAiengineCallbackArgs(callbackId, @"soundIntensity", result); + [self->callbackChannel invokeMethod:@"aiengine_callback" arguments:args]; + }; + listener.onOther = ^(NSString * _Nonnull tokenId, ChivoxAIEvalResult * _Nonnull result) { + id args = _MakeAiengineCallbackArgs(callbackId, @"other", result); + [self->callbackChannel invokeMethod:@"aiengine_callback" arguments:args]; + }; + + ChivoxAIRetValue * ret = [engine start:audioSrc tokenId:tokenId param:param listener:listener]; + if (ret != nil && [ret errId] != 0) { + result([FlutterError errorWithCode:[NSString stringWithFormat:@"%d", [ret errId]] message:[ret error] details:nil]); + return; + } + + result([NSString stringWithString:tokenId]); +} + +/** + handle_aiengineFeed + @param call + call.arguments[0]: engineId; + call.arguments[1]: bytes Uint8List; + call.arguments[2]: length int; + @param result void / FlutterError + */ +- (void)handle_aiengineFeed:(FlutterMethodCall*)call result:(FlutterResult)result { + NSString *engineId = (NSString *) call.arguments[0]; + FlutterStandardTypedData *bytes = (FlutterStandardTypedData *) call.arguments[1]; + NSNumber *length = (NSNumber *)call.arguments[2]; + + if ([length intValue] <= 0) { + result(nil); + return; + } + + ChivoxAIEngine *engine = [self->engineDic objectForKey:engineId]; + if (engine == nil) { + int eid = -1; + id emsg = @"the engineId doesn't represent any engine"; + result([FlutterError errorWithCode:[NSString stringWithFormat:@"%d", eid] message:emsg details:nil]); + return; + }; + + NSData *data = [bytes data]; + ChivoxAIRetValue *ret = [engine feed:[data bytes] length:[length intValue]]; + if (ret != nil && [ret errId] != 0) { + result([FlutterError errorWithCode:[NSString stringWithFormat:@"%d", [ret errId]] message:[ret error] details:nil]); + return; + } + + result(nil); +} + +/** + handle_aiengineStop + @param call + call.arguments[0]: engineId; + @param result void / FlutterError + */ +- (void)handle_aiengineStop:(FlutterMethodCall*)call result:(FlutterResult)result { + NSString *engineId = (NSString *) call.arguments[0]; + ChivoxAIEngine *engine = [self->engineDic objectForKey:engineId]; + if (engine == nil) { + int eid = -1; + id emsg = @"the engineId doesn't represent any engine"; + result([FlutterError errorWithCode:[NSString stringWithFormat:@"%d", eid] message:emsg details:nil]); + return; + }; + + ChivoxAIRetValue *ret = [engine stop]; + if (ret != nil && [ret errId] != 0) { + result([FlutterError errorWithCode:[NSString stringWithFormat:@"%d", [ret errId]] message:[ret error] details:nil]); + return; + } + + result(nil); +} + +/** + handle_aiengineCancel + @param call + call.arguments[0]: engineId; + @param result void / FlutterError + */ +- (void)handle_aiengineCancel:(FlutterMethodCall*)call result:(FlutterResult)result { + NSString *engineId = (NSString *) call.arguments[0]; + ChivoxAIEngine *engine = [self->engineDic objectForKey:engineId]; + if (engine == nil) { + int eid = -1; + id emsg = @"the engineId doesn't represent any engine"; + result([FlutterError errorWithCode:[NSString stringWithFormat:@"%d", eid] message:emsg details:nil]); + return; + }; + + [engine cancel]; + result(nil); + return; +} + +/** + handle_getDeviceId + @param call no args + @param result string / FlutterError + */ +- (void)handle_getDeviceId:(FlutterMethodCall*)call result:(FlutterResult)result { + NSString *devid = [ChivoxAIEngine getDeviceId]; + if (devid == nil) devid = @""; + result(devid); +} + +/** + handle_getSerialNumber + @param call + call.arguments[0]: engineId; unused; + call.arguments[1]: input Map; + @param result string / FlutterError + */ +- (void)handle_getSerialNumber:(FlutterMethodCall*)call result:(FlutterResult)result { + // unused call.arguments[0] + id input = call.arguments[1]; + + NSDictionary *output = [ChivoxAIEngine getSerialNumber:input]; + if (output == nil) { + int eid = -1; + id emsg = @"the platform interface return null"; + result([FlutterError errorWithCode:[NSString stringWithFormat:@"%d", eid] message:emsg details:nil]); + return; + } + + NSError *jerr = nil; + NSData *data = [NSJSONSerialization dataWithJSONObject:output options:0 error:&jerr]; + if (jerr != nil) { + int eid = -1; + id emsg = @"the platform interface do json serialization failed"; + result([FlutterError errorWithCode:[NSString stringWithFormat:@"%d", eid] message:emsg details:nil]); + return; + } + + result([[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]); +} + +/** + handle_clearSavedSerialNumber + @param call + call.arguments[0]: appKey; + @param result bool / FlutterError + */ +- (void)handle_clearSavedSerialNumber:(FlutterMethodCall*)call result:(FlutterResult)result { + NSString *appKey = call.arguments[0]; + BOOL output = [ChivoxAIEngine clearSavedSerialNumber:appKey]; + result(@(output)); +} + +/** + handle_getProvision + @param call + call.arguments[0]: engineId; unused; + call.arguments[1]: input Map; + @param result string / FlutterError + */ +- (void)handle_getProvision:(FlutterMethodCall*)call result:(FlutterResult)result { + // unused call.arguments[0] + id input = call.arguments[1]; + + NSDictionary *output = [ChivoxAIEngine getProvision:input]; + if (output == nil) { + int eid = -1; + id emsg = @"the platform interface return null"; + result([FlutterError errorWithCode:[NSString stringWithFormat:@"%d", eid] message:emsg details:nil]); + return; + } + + NSError *jerr = nil; + NSData *data = [NSJSONSerialization dataWithJSONObject:output options:0 error:&jerr]; + if (jerr != nil) { + int eid = -1; + id emsg = @"the platform interface do json serialization failed"; + result([FlutterError errorWithCode:[NSString stringWithFormat:@"%d", eid] message:emsg details:nil]); + return; + } + + result([[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]); +} + +- (void)handle_toolExtractRes:(FlutterMethodCall*)call result:(FlutterResult)result { + NSString *assetPrefix = call.arguments[0]; + NSArray *assetNames = call.arguments[1]; + NSString *targetRoot = call.arguments[2]; + NSString *callbackId = call.arguments[3]; + + NSString *key = [self->registrar lookupKeyForAsset:assetPrefix]; + NSString *prefix = [[NSBundle mainBundle] pathForResource:key ofType:nil]; + if (prefix == nil) { + int errId = -1; + id errMsg = [NSString stringWithFormat:@"extrace res fail: invalid assetPrefix"]; + result([FlutterError errorWithCode:[NSString stringWithFormat:@"%d", errId] message:errMsg details:nil]); + return; + } + + ChivoxAIResTool *tool = [[ChivoxAIResTool alloc] init]; + tool.onProgress = ^(float i) { + id args = @[ + callbackId, + [NSNumber numberWithFloat:i], + ]; + [self->callbackChannel invokeMethod:@"aiengine_callback" arguments:args]; + }; + + id err = [tool extract:prefix assets:[assetNames mutableCopy] target:targetRoot]; + if (err != nil) { + int errId = -1; + id errMsg = [NSString stringWithFormat:@"extrace res fail: %@", err]; + result([FlutterError errorWithCode:[NSString stringWithFormat:@"%d", errId] message:errMsg details:nil]); + return; + } + + result(nil); +} + +- (void)handle_toolLoadNativeCfg:(FlutterMethodCall*)call result:(FlutterResult)result { + NSString *resRoot = call.arguments[0]; + NSArray *resNames = call.arguments[1]; + + ChivoxAIResTool *tool = [[ChivoxAIResTool alloc] init]; + NSDictionary *dic = [tool loadNativeCfg:resRoot :[resNames mutableCopy]]; + if (dic == nil) { + result(nil); + return; + } + + NSError *jerr = nil; + NSData *data = [NSJSONSerialization dataWithJSONObject:dic options:0 error:&jerr]; + if (jerr != nil) { + int eid = -1; + id emsg = @"the platform interface do json serialization failed"; + result([FlutterError errorWithCode:[NSString stringWithFormat:@"%d", eid] message:emsg details:nil]); + return; + } + + result([[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]); +} + +@end diff --git a/packages/chivox_aiengine/ios/Classes/Lib/libCAIEngine.a b/packages/chivox_aiengine/ios/Classes/Lib/libCAIEngine.a new file mode 100644 index 0000000..4fa2617 --- /dev/null +++ b/packages/chivox_aiengine/ios/Classes/Lib/libCAIEngine.a diff --git a/packages/chivox_aiengine/ios/Classes/Lib/libaiengine.a b/packages/chivox_aiengine/ios/Classes/Lib/libaiengine.a new file mode 100644 index 0000000..8f5ebbd --- /dev/null +++ b/packages/chivox_aiengine/ios/Classes/Lib/libaiengine.a diff --git a/packages/chivox_aiengine/ios/chivox_aiengine.podspec b/packages/chivox_aiengine/ios/chivox_aiengine.podspec new file mode 100644 index 0000000..4136165 --- /dev/null +++ b/packages/chivox_aiengine/ios/chivox_aiengine.podspec @@ -0,0 +1,31 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint chivox_aiengine.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'chivox_aiengine' + s.version = '1.0.0' + s.summary = 'Chivox AIEngine Flutter SDK.' + s.description = <<-DESC +A new Flutter plugin project. + DESC + s.homepage = 'https://www.chivox.com/' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*.{h,m}' + s.public_header_files = 'Classes/**/*.h' + s.vendored_libraries = 'Classes/Lib/*.{a}' + s.static_framework = true + s.libraries = 'c++', 'z', 'sqlite3' + s.frameworks = 'SystemConfiguration' + s.dependency 'Flutter' + s.platform = :ios, '11.0' + + # Flutter.framework does not contain a i386 slice. + # 官方静态库的 arm64 slice 是设备架构;模拟器使用 x86_64 slice。 + s.pod_target_xcconfig = { + 'DEFINES_MODULE' => 'YES', + 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64 i386' + } +end diff --git a/packages/chivox_aiengine/lib/chivox_aiengine.dart b/packages/chivox_aiengine/lib/chivox_aiengine.dart new file mode 100644 index 0000000..62fce3e --- /dev/null +++ b/packages/chivox_aiengine/lib/chivox_aiengine.dart @@ -0,0 +1,274 @@ +import 'package:flutter/services.dart'; + +import 'chivox_aiengine_platform_interface.dart'; + +class ChivoxAiengineResult { + /// 本次评测唯一标识 + String tokenId = ""; + + /// 是否是本次评测的最后一个结果 + bool isLast = false; + + /// 文本结果。当结果类型是EvalResult、Error、Vad、SoundIntensity、Other时,使用本字段获取文本结果。 + String? text; + + /// 二进制结果。当结果类型是BinaryResult时,使用本字段获取二进制数据。 + Uint8List? data; + + /// 如果录音文件保存成功,返回录音文件的路径,否则返回null。 + String? recFilePath; +} + +class ChivoxAiengineResultListener { + ChivoxAiengineResultListener( + {void Function(ChivoxAiengineResult result)? onEvalResult, + void Function(ChivoxAiengineResult result)? onBinaryResult, + void Function(ChivoxAiengineResult result)? onError, + void Function(ChivoxAiengineResult result)? onVad, + void Function(ChivoxAiengineResult result)? onSoundIntensity, + void Function(ChivoxAiengineResult result)? onOther}) { + if (onEvalResult != null) this.onEvalResult = onEvalResult; + if (onBinaryResult != null) this.onBinaryResult = onBinaryResult; + if (onError != null) this.onError = onError; + if (onVad != null) this.onVad = onVad; + if (onSoundIntensity != null) this.onSoundIntensity = onSoundIntensity; + if (onOther != null) this.onOther = onOther; + } + + /// 用于接收评测分数结果 + void Function(ChivoxAiengineResult result) onEvalResult = (result) {}; + + /// 用于接收二进制结果 + void Function(ChivoxAiengineResult result) onBinaryResult = (result) {}; + + /// 用于接收错误 + void Function(ChivoxAiengineResult result) onError = (result) {}; + + /// 用于接收语音活动检测结果 + void Function(ChivoxAiengineResult result) onVad = (result) {}; + + /// 用于结果音强结果 + void Function(ChivoxAiengineResult result) onSoundIntensity = (result) {}; + + /// 其他未处理的结果,在SDK正确实现的情况下,不会产生此结果 + void Function(ChivoxAiengineResult result) onOther = (result) {}; +} + +class ChivoxAiengine { + static int _callbackCnt = 0; + static String _nextCallbackId() { + _callbackCnt++; + final cnt = _callbackCnt; + final unixTime = DateTime.now().millisecondsSinceEpoch ~/ 1000; + return "${unixTime}_$cnt"; + } + + static bool _staticInited = false; + static const _callbackChannel = MethodChannel("com.chivox.aiengine/callback"); + static final Map)> _callbacks = {}; + + static void staticInit() { + if (_staticInited) return; + _staticInited = true; + _callbackChannel.setMethodCallHandler((MethodCall call) async { + if (call.method == "aiengine_callback") { + String callbackId = call.arguments[0]; + _callbacks[callbackId]?.call(call.arguments); + } + }); + } + + bool _loadingOrLoaded = false; + String? _engineId; + + ChivoxAiengine._() { + staticInit(); + } + + /// 无用。忽略本接口 + static Future getPlatformVersion() async { + return await ChivoxAienginePlatform.instance.getPlatformVersion(); + } + + /// 创建引擎 + /// 参数cfg:引擎配置,json字符串(同ios或android端sdk) + /// 抛出异常:PlatformException + static Future create(String cfg) async { + final engine = ChivoxAiengine._(); + await engine._load(cfg); + return engine; + } + + Future _load(String cfg) async { + if (_loadingOrLoaded) { + throw PlatformException( + code: "-1", message: "don't repeatedly load the engine"); + } + _loadingOrLoaded = true; + _engineId = await ChivoxAienginePlatform.instance.aiengineNew(cfg); + } + + /// 销毁引擎 + /// 当应用程序不再需要使用评测引擎时,须销毁,否则引擎内部资源不会得到释放 + /// 抛出异常:PlatformException + Future destroy() async { + final engineId = _engineId; + if (engineId == null) return; + _engineId = null; + await ChivoxAienginePlatform.instance.aiengineDelete(engineId); + } + + /// 启动一次评测 + /// 参数audioSrc: 选择录音模式。其格式如下: + /// { + /// "srcType": string, // [必填项] 音频源:可填"innerRecorder"-sdk内部录音, "outerFeed"-用于外部输入音频 + /// "innerRecorderParam": { // [可选项] 内部录音参数 + /// "duration": int, // [可选项] 录音时长,默认永久 + /// "channel": int, // [可选项] 通道数,默认1 + /// "sampleBytes": int, // [可选项] 采样字节,默认2 + /// "sampleRate": int, // [可选项] 采样率,默认16000 + /// "saveFile": string, // [可选项] 保存录音文件的路径,默认不保存 + /// } + /// } + /// 参数param: 语音评测参数,json字符串(同ios或android端sdk) + /// 参数listener: 用于接收评测结果的listener对象 + /// 抛出异常:PlatformException + /// + /// 注意:对于ios平台,sdk内部并没有设置AudioSessionCategory,应用层须自行设置其为一个支持录音的值。 + Future start(Map audioSrc, String param, + ChivoxAiengineResultListener listener) async { + final engineId = _engineId; + if (engineId == null) { + throw PlatformException( + code: "-1", message: "the engine has not been loaded yet"); + } + + final callbackId = _nextCallbackId(); + _callbacks[callbackId] = (List args) { + var result = ChivoxAiengineResult(); + String type = args[1]; + result.isLast = args[2]; + result.tokenId = args[3]; + result.text = args[4]; + result.data = args[5]; + result.recFilePath = args[6]; + if (type == "evalResult") { + listener.onEvalResult(result); + } else if (type == "binResult") { + listener.onBinaryResult(result); + } else if (type == "error") { + listener.onError(result); + } else if (type == "vad") { + listener.onVad(result); + } else if (type == "soundIntensity") { + listener.onSoundIntensity(result); + } else { + listener.onOther(result); + } + + if (result.isLast || type == "error") { + _callbacks.remove(callbackId); + } + + // TODO 超过一定时间没有回调,则强行移除回调 + }; + + await ChivoxAienginePlatform.instance + .aiengineStart(engineId, audioSrc, param, callbackId); + return; + } + + /// 输入音频数据 (仅outerFeed模式可使用) + /// 抛出异常:PlatformException + Future feed(Uint8List bytes, int length) async { + final engineId = _engineId; + if (engineId == null) { + throw PlatformException( + code: "-1", message: "the engine has not been loaded yet"); + } + await ChivoxAienginePlatform.instance.aiengineFeed(engineId, bytes, length); + } + + /// 结束录音 (或结束音频数据) + /// 本方法调用后将会进入等待评测结果状态,一旦评测结果产生后,将通过start方法的listener参数回调给应用层。 + /// 抛出异常:PlatformException + Future stop() async { + final engineId = _engineId; + if (engineId == null) { + throw PlatformException( + code: "-1", message: "the engine has not been loaded yet"); + } + await ChivoxAienginePlatform.instance.aiengineStop(engineId); + } + + /// 取消全部评测 + /// 本方法调用后,sdk内部取消所有正在进行的评测 + /// 抛出异常:PlatformException + Future cancel() async { + final engineId = _engineId; + if (engineId == null) { + throw PlatformException( + code: "-1", message: "the engine has not been loaded yet"); + } + await ChivoxAienginePlatform.instance.aiengineCancel(engineId); + } + + static Future getDeviceId() async { + return await ChivoxAienginePlatform.instance.getDeviceId(); + } + + /// 激活设备,并返回序列号 + /// 本方法调用后,sdk会把序列号存储在文件系统中。 + /// 应用层下次调用本方法sdk会直接从文件系统中取出该值返回给应用层。 + /// 应用层可调用clearSavedSerialNumber方法清除sdk存储的序列号。 + /// 参数input: 结构同ios或android端sdk + /// 返回: 结构同ios或andorid端sdk + /// 抛出异常:PlatformException + static Future getSerialNumber( + ChivoxAiengine? engine, Map input) async { + return await ChivoxAienginePlatform.instance + .getSerialNumber(engine?._engineId, input); + } + + /// 清除sdk在文件系统中存储的序列号 + /// 抛出异常:PlatformException + static Future clearSavedSerialNumber(String appKey) async { + return await ChivoxAienginePlatform.instance.clearSavedSerialNumber(appKey); + } + + /// 某些appkey支持二次授权功能,本方法提供该功能 + /// 抛出异常:PlatformException + static Future getProvision(ChivoxAiengine? engine, Map input) async { + return await ChivoxAienginePlatform.instance + .getProvision(engine?._engineId, input); + } + + /// 工具方法:提取sdk离线资源包到一个目录中 + /// 参数assetPrefix: 离线资源包在flutter assets中的前缀 + /// 参数assetNames: 需要提取的资源包的名称列表 + /// 参数targetDir: 目标目录,资源包会解压于此 + /// 参数progressHint: 解压进度回调 + /// 抛出异常:PlatformException + static Future extractRes(String assetPrefix, List assetNames, + String targetDir, Function(double) progressHint) async { + staticInit(); + final callbackId = _nextCallbackId(); + _callbacks[callbackId] = (List args) { + progressHint(args[1]); + }; + + await ChivoxAienginePlatform.instance + .extractRes(assetPrefix, assetNames, targetDir, callbackId); + _callbacks.remove(callbackId); + } + + /// 工具方法:根据传入的资源根目录和资源名称列表,加载离线资源配置 + /// 参数resRootDir: 离线资源包的根目录 + /// 参数resNames: 指定需要哪些离线资源包 + /// 返回: 一个json字符串,描述了离线资源配置信息。该json用于引擎加载时提供离线配置信息。 + /// 抛出异常:PlatformException + static Future loadNativeCfg( + String resRootDir, List resNames) { + return ChivoxAienginePlatform.instance.loadNativeCfg(resRootDir, resNames); + } +} diff --git a/packages/chivox_aiengine/lib/chivox_aiengine_method_channel.dart b/packages/chivox_aiengine/lib/chivox_aiengine_method_channel.dart new file mode 100644 index 0000000..9538b42 --- /dev/null +++ b/packages/chivox_aiengine/lib/chivox_aiengine_method_channel.dart @@ -0,0 +1,104 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +import 'chivox_aiengine_platform_interface.dart'; + +/// An implementation of [ChivoxAienginePlatform] that uses method channels. +class MethodChannelChivoxAiengine extends ChivoxAienginePlatform { + /// The method channel used to interact with the native platform. + @visibleForTesting + final methodChannel = const MethodChannel('com.chivox.aiengine/call'); + + @override + Future getPlatformVersion() async { + final version = + await methodChannel.invokeMethod('getPlatformVersion'); + return version; + } + + @override + Future aiengineNew(String cfg) async { + final engineId = + await methodChannel.invokeMethod('aiengineNew', [cfg]); + return engineId; + } + + @override + Future aiengineDelete(String engineId) async { + await methodChannel.invokeMethod('aiengineDelete', [engineId]); + return; + } + + @override + Future aiengineStart(String engineId, Map audioSrc, + String param, String callbackId) async { + await methodChannel.invokeMethod( + 'aiengineStart', [engineId, audioSrc, param, callbackId]); + return; + } + + @override + Future aiengineFeed( + String engineId, Uint8List bytes, int length) async { + await methodChannel + .invokeMethod('aiengineFeed', [engineId, bytes, length]); + return; + } + + @override + Future aiengineStop(String engineId) async { + await methodChannel.invokeMethod('aiengineStop', [engineId]); + return; + } + + @override + Future aiengineCancel(String engineId) async { + await methodChannel.invokeMethod('aiengineCancel', [engineId]); + return; + } + + @override + Future getDeviceId() async { + final devId = await methodChannel.invokeMethod( + 'getDeviceId', + ); + return devId; + } + + @override + Future getSerialNumber(String? engineId, Map input) async { + final output = await methodChannel + .invokeMethod('getSerialNumber', [engineId, input]); + return output; + } + + @override + Future clearSavedSerialNumber(String appKey) async { + final ret = await methodChannel + .invokeMethod('clearSavedSerialNumber', [appKey]); + return ret; + } + + @override + Future getProvision(String? engineId, Map input) async { + final output = await methodChannel + .invokeMethod('getProvision', [engineId, input]); + return output; + } + + @override + Future extractRes(String assetPrefix, List assetNames, + String targetDir, String callbackId) async { + await methodChannel.invokeMethod( + 'extractRes', [assetPrefix, assetNames, targetDir, callbackId]); + return; + } + + @override + Future loadNativeCfg( + String resRootDir, List resNames) async { + final cfgStr = await methodChannel + .invokeMethod('loadNativeCfg', [resRootDir, resNames]); + return cfgStr; + } +} diff --git a/packages/chivox_aiengine/lib/chivox_aiengine_platform_interface.dart b/packages/chivox_aiengine/lib/chivox_aiengine_platform_interface.dart new file mode 100644 index 0000000..4102dfa --- /dev/null +++ b/packages/chivox_aiengine/lib/chivox_aiengine_platform_interface.dart @@ -0,0 +1,82 @@ +import 'dart:typed_data'; + +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import 'chivox_aiengine_method_channel.dart'; + +abstract class ChivoxAienginePlatform extends PlatformInterface { + /// Constructs a ChivoxAienginePlatform. + ChivoxAienginePlatform() : super(token: _token); + + static final Object _token = Object(); + + static ChivoxAienginePlatform _instance = MethodChannelChivoxAiengine(); + + /// The default instance of [ChivoxAienginePlatform] to use. + /// + /// Defaults to [MethodChannelChivoxAiengine]. + static ChivoxAienginePlatform get instance => _instance; + + /// Platform-specific implementations should set this with their own + /// platform-specific class that extends [ChivoxAienginePlatform] when + /// they register themselves. + static set instance(ChivoxAienginePlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + Future getPlatformVersion() { + throw UnimplementedError('platformVersion() has not been implemented.'); + } + + Future aiengineNew(String cfg) { + throw UnimplementedError('aiengineNew() has not been implemented.'); + } + + Future aiengineDelete(String engineId) { + throw UnimplementedError('aiengineDelete() has not been implemented.'); + } + + Future aiengineStart(String engineId, Map audioSrc, + String param, String callbackId) { + throw UnimplementedError('aiengineStart() has not been implemented.'); + } + + Future aiengineFeed(String engineId, Uint8List bytes, int length) { + throw UnimplementedError('aiengineFeed() has not been implemented.'); + } + + Future aiengineStop(String engineId) { + throw UnimplementedError('aiengineStop() has not been implemented.'); + } + + Future aiengineCancel(String engineId) { + throw UnimplementedError('aiengineCancel() has not been implemented.'); + } + + Future getDeviceId() { + throw UnimplementedError('getDeviceId() has not been implemented.'); + } + + Future getSerialNumber(String? engineId, Map input) { + throw UnimplementedError('getSerialNumber() has not been implemented.'); + } + + Future clearSavedSerialNumber(String appKey) { + throw UnimplementedError( + 'clearSavedSerialNumber() has not been implemented.'); + } + + Future getProvision(String? engineId, Map input) { + throw UnimplementedError('getProvision() has not been implemented.'); + } + + Future extractRes(String assetPrefix, List assetNames, + String targetDir, String callbackId) { + throw UnimplementedError('extractRes() has not been implemented.'); + } + + Future loadNativeCfg(String resRootDir, List resNames) { + throw UnimplementedError('loadNativeCfg() has not been implemented.'); + } +} diff --git a/packages/chivox_aiengine/pubspec.yaml b/packages/chivox_aiengine/pubspec.yaml new file mode 100644 index 0000000..35395d2 --- /dev/null +++ b/packages/chivox_aiengine/pubspec.yaml @@ -0,0 +1,72 @@ +name: chivox_aiengine +description: Chivox AIEngine Flutter plugin vendored for Wow English. +version: 1.0.0 +homepage: https://www.chivox.com/ + +environment: + sdk: '>=3.0.6 <4.0.0' + flutter: ">=3.3.0" + +dependencies: + flutter: + sdk: flutter + plugin_platform_interface: ^2.0.2 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + # This section identifies this Flutter project as a plugin project. + # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) + # which should be registered in the plugin registry. This is required for + # using method channels. + # The Android 'package' specifies package in which the registered class is. + # This is required for using method channels on Android. + # The 'ffiPlugin' specifies that native code should be built and bundled. + # This is required for using `dart:ffi`. + # All these are used by the tooling to maintain consistency when + # adding or updating assets for this project. + plugin: + platforms: + android: + package: com.example.chivox_aiengine + pluginClass: ChivoxAienginePlugin + ios: + pluginClass: ChivoxAienginePlugin + + # To add assets to your plugin package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # To add custom fonts to your plugin package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/pubspec.yaml b/pubspec.yaml index 991b20e..500e004 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -94,6 +94,9 @@ dependencies: extended_text: ^11.0.1 # 视频播放 https://pub.dev/packages/video_player video_player: ^2.8.6 + # 驰声语音评测 SDK(官方离线包裁剪为当前支持架构) + chivox_aiengine: + path: packages/chivox_aiengine # UI适配 https://pub.dev/packages/responsive_framework responsive_framework: ^1.0.0 # 音频播放 https://pub.dev/packages/audioplayers @@ -155,6 +158,7 @@ flutter: assets: - assets/images/ + - assets/chivox/ - assets/fonts/ - assets/sounds/ - assets/lotties/