diff --git a/app/build.gradle b/app/build.gradle
index 4833ffcd72..ef15af78ba 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -9,7 +9,7 @@ apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply plugin: ly.count.android.plugins.UploadSymbolsPlugin
-apply from: rootProject.file('gradle/bytex/bytex.gradle')
+//apply from: rootProject.file('gradle/bytex/bytex.gradle')
Properties properties = new Properties()
properties.load(project.rootProject.file("gradle.properties").newDataInputStream())
@@ -219,7 +219,7 @@ dependencies {
implementation project(':core:mogo-core-function-call')
implementation project(':core:mogo-core-utils')
implementation project(':core:mogo-core-res')
- implementation project(':tts:tts-iflytek')
+ implementation project(':tts:tts-iflytek-offline')
androidTestImplementation project(':core:mogo-core-function-call')
androidTestImplementation project(':core:mogo-core-res')
diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/util/FileUtils.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/util/FileUtils.java
index 0f5e4eb7d1..034291efec 100644
--- a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/util/FileUtils.java
+++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/util/FileUtils.java
@@ -24,6 +24,8 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.net.URL;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
@@ -1818,6 +1820,26 @@ public final class FileUtils {
}).start();
}
+ public static void writeFile(String path, byte[] bytes) {
+ boolean append = false;
+ try {
+ File file = new File(path);
+ if (file.exists()) {
+ append = true;
+ }else {
+ file.createNewFile();
+ }
+ FileOutputStream out = new FileOutputStream(path,true);//指定写到哪个路径中
+ FileChannel fileChannel = out.getChannel();
+ fileChannel.write(ByteBuffer.wrap(bytes)); //将字节流写入文件中
+ fileChannel.force(true);//强制刷新
+ fileChannel.close();
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
/**
* 文件拷贝监听
diff --git a/foudations/mogo-commons/src/main/java/com/mogo/commons/voice/AIAssist.java b/foudations/mogo-commons/src/main/java/com/mogo/commons/voice/AIAssist.java
index e0df6f7ff4..69917234db 100644
--- a/foudations/mogo-commons/src/main/java/com/mogo/commons/voice/AIAssist.java
+++ b/foudations/mogo-commons/src/main/java/com/mogo/commons/voice/AIAssist.java
@@ -61,11 +61,17 @@ public class AIAssist {
Class> clazz1 = null;
Class> clazz2 = null;
Class> clazz3 = null;
+ Class> clazz4 = null;
try {
clazz1 = Class.forName("com.mogo.tts.pad.PadTTS");
} catch (Exception ignored) {
}
+ try {
+ clazz4 = Class.forName("com.mogo.tts.iflytekoffline.IFlyTekOfflineTts");
+ } catch (Exception ignored) {
+ }
+
try {
clazz2 = Class.forName("com.mogo.tts.iflytek.IFlyTekTts");
} catch (Exception ignored) {
@@ -77,10 +83,12 @@ public class AIAssist {
if (clazz1 != null) {
mTTS = (IMogoTTS) clazz1.getConstructor().newInstance();
- } else if (clazz2 != null) {
- mTTS = (IMogoTTS) clazz2.getConstructor().newInstance();
+ } else if (clazz4 != null) {
+ mTTS = (IMogoTTS) clazz4.getConstructor().newInstance();
} else if (clazz3 != null) {
mTTS = (IMogoTTS) clazz3.getConstructor().newInstance();
+ } else {
+ mTTS = (IMogoTTS) clazz2.getConstructor().newInstance();
}
if (mTTS != null) {
mTTS.init(context);
diff --git a/libraries/mogo-speech/build/intermediates/aapt_friendly_merged_manifests/release/aapt/AndroidManifest.xml b/libraries/mogo-speech/build/intermediates/aapt_friendly_merged_manifests/release/aapt/AndroidManifest.xml
new file mode 100644
index 0000000000..fd77df7ff4
--- /dev/null
+++ b/libraries/mogo-speech/build/intermediates/aapt_friendly_merged_manifests/release/aapt/AndroidManifest.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index 511fc32127..5f917d759f 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -53,6 +53,7 @@ include ':libraries:mogo-speech'
include ':tts:tts-base'
include ':tts:tts-pad'
include ':tts:tts-iflytek'
+include ':tts:tts-iflytek-offline'
include ':tts:tts-zhi'
// 测试DEBUG
diff --git a/tts/tts-iflytek-offline/.gitignore b/tts/tts-iflytek-offline/.gitignore
new file mode 100644
index 0000000000..42afabfd2a
--- /dev/null
+++ b/tts/tts-iflytek-offline/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/tts/tts-iflytek-offline/build.gradle b/tts/tts-iflytek-offline/build.gradle
new file mode 100644
index 0000000000..df5088d396
--- /dev/null
+++ b/tts/tts-iflytek-offline/build.gradle
@@ -0,0 +1,65 @@
+plugins {
+ id 'com.android.library'
+ id 'kotlin-android'
+ id 'kotlin-android-extensions'
+ id 'kotlin-kapt'
+}
+
+android {
+ compileSdkVersion rootProject.ext.android.compileSdkVersion
+ // buildToolsVersion rootProject.ext.android.buildToolsVersion
+ defaultConfig {
+ minSdkVersion rootProject.ext.android.minSdkVersion
+ targetSdkVersion rootProject.ext.android.targetSdkVersion
+ versionCode Integer.valueOf(VERSION_CODE)
+ versionName getValueFromRootProperties("${project.name.replace("-", "_").toUpperCase()}_VERSION")
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles "consumer-rules.pro"
+
+ //ARouter apt 参数
+ kapt {
+ useBuildCache = false
+ arguments {
+ arg("AROUTER_MODULE_NAME", project.getName())
+ }
+ }
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ lintOptions {
+ abortOnError false
+ }
+}
+
+dependencies {
+ implementation fileTree(dir: "libs", include: ["*.jar"])
+ implementation files('libs/AIKit.aar')
+ implementation rootProject.ext.dependencies.androidxappcompat
+
+ implementation rootProject.ext.dependencies.arouter
+ kapt rootProject.ext.dependencies.aroutercompiler
+
+ api rootProject.ext.dependencies.aiassist
+
+ if (Boolean.valueOf(USE_MAVEN_PACKAGE)) {
+ implementation rootProject.ext.dependencies.ttsbase
+ implementation rootProject.ext.dependencies.mogo_core_utils
+ } else {
+ implementation project(":tts:tts-base")
+ implementation project(':core:mogo-core-utils')
+ }
+}
+
+apply from: new File(rootProject.rootDir, "gradle/upload.gradle").toString()
\ No newline at end of file
diff --git a/tts/tts-iflytek-offline/consumer-rules.pro b/tts/tts-iflytek-offline/consumer-rules.pro
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tts/tts-iflytek-offline/gradle.properties b/tts/tts-iflytek-offline/gradle.properties
new file mode 100644
index 0000000000..1eb7731d01
--- /dev/null
+++ b/tts/tts-iflytek-offline/gradle.properties
@@ -0,0 +1,3 @@
+GROUP=com.mogo.tts
+POM_ARTIFACT_ID=tts-iflytek-offline
+VERSION_CODE=1
diff --git a/tts/tts-iflytek-offline/libs/AIKit.aar b/tts/tts-iflytek-offline/libs/AIKit.aar
new file mode 100644
index 0000000000..ca6f61fff4
Binary files /dev/null and b/tts/tts-iflytek-offline/libs/AIKit.aar differ
diff --git a/tts/tts-iflytek-offline/proguard-rules.pro b/tts/tts-iflytek-offline/proguard-rules.pro
new file mode 100644
index 0000000000..481bb43481
--- /dev/null
+++ b/tts/tts-iflytek-offline/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/tts/tts-iflytek-offline/src/main/AndroidManifest.xml b/tts/tts-iflytek-offline/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..399e7a06a8
--- /dev/null
+++ b/tts/tts-iflytek-offline/src/main/AndroidManifest.xml
@@ -0,0 +1,3 @@
+
+
\ No newline at end of file
diff --git a/tts/tts-iflytek-offline/src/main/assets/xTTS/e05d571cc_1.0.0_xTTS_CnCn_xiaoyan_2018_fix_arm.dat b/tts/tts-iflytek-offline/src/main/assets/xTTS/e05d571cc_1.0.0_xTTS_CnCn_xiaoyan_2018_fix_arm.dat
new file mode 100644
index 0000000000..66cd22bf14
Binary files /dev/null and b/tts/tts-iflytek-offline/src/main/assets/xTTS/e05d571cc_1.0.0_xTTS_CnCn_xiaoyan_2018_fix_arm.dat differ
diff --git a/tts/tts-iflytek-offline/src/main/assets/xTTS/e3fe94474_1.0.0_xTTS_CnCn_xiaoyan_2018_arm.irf b/tts/tts-iflytek-offline/src/main/assets/xTTS/e3fe94474_1.0.0_xTTS_CnCn_xiaoyan_2018_arm.irf
new file mode 100644
index 0000000000..4fff5ba5c9
Binary files /dev/null and b/tts/tts-iflytek-offline/src/main/assets/xTTS/e3fe94474_1.0.0_xTTS_CnCn_xiaoyan_2018_arm.irf differ
diff --git a/tts/tts-iflytek-offline/src/main/assets/xTTS/e4b08c6f3_1.0.0_xTTS_CnCn_xiaofeng_2018_fix_arm.dat b/tts/tts-iflytek-offline/src/main/assets/xTTS/e4b08c6f3_1.0.0_xTTS_CnCn_xiaofeng_2018_fix_arm.dat
new file mode 100644
index 0000000000..cb840a9725
Binary files /dev/null and b/tts/tts-iflytek-offline/src/main/assets/xTTS/e4b08c6f3_1.0.0_xTTS_CnCn_xiaofeng_2018_fix_arm.dat differ
diff --git a/tts/tts-iflytek-offline/src/main/assets/xTTS/e4caee636_1.0.2_xTTS_CnCn_front_Emb_arm_2017.irf b/tts/tts-iflytek-offline/src/main/assets/xTTS/e4caee636_1.0.2_xTTS_CnCn_front_Emb_arm_2017.irf
new file mode 100644
index 0000000000..83c17c1f9b
Binary files /dev/null and b/tts/tts-iflytek-offline/src/main/assets/xTTS/e4caee636_1.0.2_xTTS_CnCn_front_Emb_arm_2017.irf differ
diff --git a/tts/tts-iflytek-offline/src/main/assets/xTTS/ebdbd61ae_1.0.0_xTTS_CnCn_xiaofeng_2018_arm.irf b/tts/tts-iflytek-offline/src/main/assets/xTTS/ebdbd61ae_1.0.0_xTTS_CnCn_xiaofeng_2018_arm.irf
new file mode 100644
index 0000000000..83d93a047a
Binary files /dev/null and b/tts/tts-iflytek-offline/src/main/assets/xTTS/ebdbd61ae_1.0.0_xTTS_CnCn_xiaofeng_2018_arm.irf differ
diff --git a/tts/tts-iflytek-offline/src/main/java/com/mogo/tts/iflytekoffline/AudioTrackManager.kt b/tts/tts-iflytek-offline/src/main/java/com/mogo/tts/iflytekoffline/AudioTrackManager.kt
new file mode 100644
index 0000000000..c147146962
--- /dev/null
+++ b/tts/tts-iflytek-offline/src/main/java/com/mogo/tts/iflytekoffline/AudioTrackManager.kt
@@ -0,0 +1,229 @@
+package com.mogo.tts.iflytekoffline
+
+import android.media.AudioFormat
+import android.media.AudioManager
+import android.media.AudioRecord
+import android.media.AudioTrack
+import android.os.Process
+import java.io.DataInputStream
+import java.io.File
+import java.io.FileInputStream
+
+class AudioTrackManager {
+ private var mAudioTrack: AudioTrack? = null
+ private var mDis: DataInputStream? = null //播放文件的数据流
+ private var mRecordThread: Thread? = null
+ private var isStart = false
+
+ //指定缓冲区大小。调用AudioRecord类的getMinBufferSize方法可以获得。
+ private var mMinBufferSize = 0
+ private var mSampleRate = 16000
+
+ private var listener: OnCompleteListener? = null
+
+ enum class sampleRateType {
+ SAMPLE_RATE_16k,
+ SAMPLE_RATE_24k
+ }
+
+ private fun initData() {
+ //根据采样率,采样精度,单双声道来得到frame的大小。
+ mMinBufferSize =
+ AudioTrack.getMinBufferSize(mSampleRate, mChannelConfig, mAudioFormat) //计算最小缓冲区
+ //注意,按照数字音频的知识,这个算出来的是一秒钟buffer的大小。
+ //创建AudioTrack
+ mAudioTrack = AudioTrack(
+ mStreamType, mSampleRate, mChannelConfig,
+ mAudioFormat, mMinBufferSize, mMode
+ )
+ }
+
+ fun setListener(listener: OnCompleteListener) {
+ this.listener = listener
+ }
+
+ fun setSampleRate(sampleRate: sampleRateType?) {
+ when (sampleRate) {
+ sampleRateType.SAMPLE_RATE_16k -> mSampleRate = mSampleRateIn16KHz
+ sampleRateType.SAMPLE_RATE_24k -> mSampleRate = mSampleRateIn24KHz
+ else -> {
+ mSampleRate = mSampleRateIn24KHz
+ }
+ }
+ }
+
+ /**
+ * 销毁线程方法
+ */
+ private fun destroyThread() {
+ try {
+ isStart = false
+ if (null != mRecordThread && Thread.State.RUNNABLE == mRecordThread!!.state) {
+ try {
+ Thread.sleep(500)
+ mRecordThread!!.interrupt()
+ } catch (e: Exception) {
+ mRecordThread = null
+ }
+ }
+ mRecordThread = null
+ } catch (e: Exception) {
+ e.printStackTrace()
+ } finally {
+ mRecordThread = null
+ }
+ }
+
+ /**
+ * 启动播放线程
+ */
+ private fun startThread() {
+ destroyThread()
+ isStart = true
+ if (mRecordThread == null) {
+ mRecordThread = Thread(playRunnable)
+ mRecordThread!!.start()
+ }
+ }
+
+ /**
+ * 播放线程
+ */
+ var playRunnable: Runnable = Runnable {
+ try {
+ //设置线程的优先级
+ Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO)
+ val tempBuffer = ByteArray(mMinBufferSize)
+ var readCount = 0
+ while (mDis!!.available() > 0) {
+ readCount = mDis!!.read(tempBuffer)
+ if (readCount == AudioTrack.ERROR_INVALID_OPERATION || readCount == AudioTrack.ERROR_BAD_VALUE) {
+ continue
+ }
+ if (readCount != 0 && readCount != -1) { //一边播放一边写入语音数据
+ //判断AudioTrack未初始化,停止播放的时候释放了,状态就为STATE_UNINITIALIZED
+ if (mAudioTrack!!.state == AudioTrack.STATE_UNINITIALIZED) {
+ initData()
+ }
+ mAudioTrack!!.play()
+ mAudioTrack!!.write(tempBuffer, 0, readCount)
+ }
+ }
+ stopPlay() //播放完就停止播放
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ listener?.onComplete()
+ }
+
+ init {
+ initData()
+ }
+
+ /**
+ * 播放文件
+ * @param path
+ * @throws Exception
+ */
+ @Throws(Exception::class)
+ private fun setPath(path: String) {
+ val file = File(path)
+ mDis = DataInputStream(FileInputStream(file))
+ }
+
+ /**
+ * 启动播放
+ *
+ * @param path
+ */
+ fun startPlay(path: String) {
+ try {
+// //AudioTrack未初始化
+// if(mAudioTrack.getState() == AudioTrack.STATE_UNINITIALIZED){
+// throw new RuntimeException("The AudioTrack is not uninitialized");
+// }//AudioRecord.getMinBufferSize的参数是否支持当前的硬件设备
+// else if (AudioTrack.ERROR_BAD_VALUE == mMinBufferSize || AudioTrack.ERROR == mMinBufferSize) {
+// throw new RuntimeException("AudioTrack Unable to getMinBufferSize");
+// }else{
+ setPath(path)
+ startThread()
+
+ // }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+
+ /**
+ * 停止播放
+ */
+ fun stopPlay() {
+ try {
+ destroyThread() //销毁线程
+ if (mAudioTrack != null) {
+ if (mAudioTrack!!.state == AudioRecord.STATE_INITIALIZED) { //初始化成功
+ mAudioTrack!!.stop() //停止播放
+ }
+ if (mAudioTrack != null) {
+ mAudioTrack!!.release() //释放audioTrack资源
+ }
+ }
+ if (mDis != null) {
+ mDis!!.close() //关闭数据输入流
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+
+ val playState: Int
+ get() {
+ if (mAudioTrack != null) {
+ return mAudioTrack!!.playState
+ }
+ return AudioTrack.PLAYSTATE_STOPPED
+ }
+
+ companion object {
+ @Volatile
+ private var mInstance: AudioTrackManager? = null
+
+ //音频流类型
+ private const val mStreamType = AudioManager.STREAM_MUSIC
+
+ //指定采样率 (MediaRecoder 的采样率通常是8000Hz AAC的通常是44100Hz。 设置采样率为44100,目前为常用的采样率,官方文档表示这个值可以兼容所有的设置)
+ const val mSampleRateIn16KHz: Int = 16000
+ const val mSampleRateIn24KHz: Int = 24000
+
+ //指定捕获音频的声道数目。在AudioFormat类中指定用于此的常量
+ private const val mChannelConfig = AudioFormat.CHANNEL_CONFIGURATION_MONO //单声道
+
+ //指定音频量化位数 ,在AudioFormaat类中指定了以下各种可能的常量。通常我们选择ENCODING_PCM_16BIT和ENCODING_PCM_8BIT PCM代表的是脉冲编码调制,它实际上是原始音频样本。
+ //因此可以设置每个样本的分辨率为16位或者8位,16位将占用更多的空间和处理能力,表示的音频也更加接近真实。
+ private const val mAudioFormat = AudioFormat.ENCODING_PCM_16BIT
+
+ //STREAM的意思是由用户在应用程序通过write方式把数据一次一次得写到audiotrack中。这个和我们在socket中发送数据一样,
+ // 应用层从某个地方获取数据,例如通过编解码得到PCM数据,然后write到audiotrack。
+ private const val mMode = AudioTrack.MODE_STREAM
+ val instance: AudioTrackManager?
+ /**
+ * 获取单例引用
+ *
+ * @return
+ */
+ get() {
+ if (mInstance == null) {
+ synchronized(AudioTrackManager::class.java) {
+ if (mInstance == null) {
+ mInstance = AudioTrackManager()
+ }
+ }
+ }
+ return mInstance
+ }
+ }
+
+ interface OnCompleteListener {
+ fun onComplete()
+ }
+}
\ No newline at end of file
diff --git a/tts/tts-iflytek-offline/src/main/java/com/mogo/tts/iflytekoffline/IFlyTekOfflineTts.kt b/tts/tts-iflytek-offline/src/main/java/com/mogo/tts/iflytekoffline/IFlyTekOfflineTts.kt
new file mode 100644
index 0000000000..69f71b023e
--- /dev/null
+++ b/tts/tts-iflytek-offline/src/main/java/com/mogo/tts/iflytekoffline/IFlyTekOfflineTts.kt
@@ -0,0 +1,512 @@
+package com.mogo.tts.iflytekoffline
+
+import android.content.Context
+import android.os.Looper
+import android.util.Pair
+import androidx.annotation.Keep
+import com.iflytek.aikit.core.AeeEvent
+import com.iflytek.aikit.core.AiHandle
+import com.iflytek.aikit.core.AiHelper
+import com.iflytek.aikit.core.AiInput
+import com.iflytek.aikit.core.AiListener
+import com.iflytek.aikit.core.AiRequest
+import com.iflytek.aikit.core.AiResponse
+import com.iflytek.aikit.core.AiText
+import com.iflytek.aikit.core.BaseLibrary
+import com.iflytek.aikit.core.CoreListener
+import com.iflytek.aikit.core.ErrType
+import com.mogo.eagle.core.utilcode.mogo.logger.CallerLogger.d
+import com.mogo.eagle.core.utilcode.mogo.logger.CallerLogger.i
+import com.mogo.eagle.core.utilcode.util.FileUtils
+import com.mogo.eagle.core.utilcode.util.ResourceUtils
+import com.mogo.eagle.core.utilcode.util.ThreadUtils
+import com.mogo.eagle.core.utilcode.util.UiThreadHandler
+import com.mogo.tts.base.IGlobalTtsCallback
+import com.mogo.tts.base.IMogoTTS
+import com.mogo.tts.base.IMogoTTSCallback
+import com.mogo.tts.base.LangTtsEntity
+import com.mogo.tts.base.LanguageType
+import com.mogo.tts.base.MultiLangTtsEntity
+import com.mogo.tts.base.PreemptType
+import com.mogo.tts.iflytekoffline.AudioTrackManager.OnCompleteListener
+import java.io.File
+import java.util.LinkedList
+
+@Keep
+class IFlyTekOfflineTts : IMogoTTS {
+ companion object {
+ const val TAG = "IFlyTekTts"
+ }
+
+ private var context: Context? = null
+
+ private var aiHandle: AiHandle? = null
+ private val OUTPUT_DIR by lazy {
+ "/sdcard/iflytek/xtts/output"
+ }
+ private val ABILITYID by lazy {
+ "e2e44feff"
+ }
+
+ // 等级由低到高为0、1、2、3,默认为-1表示没有正在tts的
+ private var curTtsLevel = -1
+
+ // 由于主动打断不会有回调事件,所以主动打断时清掉map中被打断的text和callback
+ private var curTtsContent = ""
+ private var curTtsEntity: MultiLangTtsEntity? = null
+ private val linkedList = LinkedList>()
+
+ private val speakVoiceMap by lazy {
+ HashMap()
+ }
+
+ private var mGlobalTtsCallback: IGlobalTtsCallback? = null
+
+ override fun init(context: Context) {
+ this.context = context
+ initSDK()
+ }
+
+ override fun initTts(sn: String?) {}
+
+ private fun initSDK() {
+ val workPath = "/sdcard/iflytek/aikit/xTTS"
+ val file = File("$workPath/e3fe94474_1.0.0_xTTS_CnCn_xiaoyan_2018_arm.irf")
+ if (!file.exists() || file.length() == 0L) {
+ ThreadUtils.getIoPool().execute {
+ ResourceUtils.copyFileFromAssets("xTTS", "$workPath")
+ initEngine(workPath)
+ }
+ } else {
+ ThreadUtils.getSinglePool().execute {
+ initEngine(workPath)
+ }
+ }
+ AiHelper.getInst().registerListener(coreListener)
+ AiHelper.getInst().registerListener(ABILITYID, aiRespListener)
+ }
+
+ private fun initEngine(workPath: String) {
+ val params = BaseLibrary.Params.builder()
+ .appId("0c498b42")
+ .apiKey("8579f566eb7f3c4f4a07148ad9e2408c")
+ .apiSecret("NTRmMmI5MWI4NzIzZTIxN2Q5N2FjMWVl")
+ .ability(ABILITYID)
+ .workDir(workPath)
+ .build()
+ AiHelper.getInst().init(context, params)
+ AudioTrackManager.instance?.setSampleRate(AudioTrackManager.sampleRateType.SAMPLE_RATE_16k)
+ AudioTrackManager.instance?.setListener(completeListener)
+ }
+
+ private val coreListener = CoreListener { type, code ->
+ when (type) {
+ ErrType.AUTH -> {
+ if (code == 0) {
+ // SDK授权成功
+ d(TAG, "科大讯飞离线语音合成授权成功!")
+ } else {
+ // SDK授权失败,授权码为:code
+ d(TAG, "科大讯飞离线语音合成授权失败码:$code")
+ }
+ }
+
+ else -> {
+ // SDK状态为:type, code
+ d(TAG, "type:$type, code:$code")
+ }
+ }
+ }
+
+ private val completeListener = object : OnCompleteListener {
+ override fun onComplete() {
+ onCompleted()
+ }
+ }
+
+ override fun release() {
+ if (Thread.currentThread() != Looper.getMainLooper().thread) {
+ UiThreadHandler.post {
+ AiHelper.getInst().unInit()
+ }
+ } else {
+ AiHelper.getInst().unInit()
+ }
+ }
+
+ override fun flush() {
+ }
+
+ override fun speakTTSVoice(tts: String?) {
+
+ }
+
+ override fun speakTTSVoice(tts: String?, callback: IMogoTTSCallback?) {
+ }
+
+ override fun speakTTSVoice(tts: String?, type: PreemptType?, callback: IMogoTTSCallback?) {
+ }
+
+ override fun speakTTSVoiceWithLevel(tts: String?, level: Int) {
+ speakTTSVoiceWithLevel(tts, level, null)
+ }
+
+ override fun speakTTSVoiceWithLevel(tts: String?, level: Int, callBack: IMogoTTSCallback?) {
+ if (tts.isNullOrEmpty()) return
+ speakMultiLangTTSWithLevel(
+ MultiLangTtsEntity(
+ listOf(
+ LangTtsEntity(
+ tts,
+ LanguageType.CHINESE
+ )
+ )
+ ), level, callBack
+ )
+ }
+
+ override fun speakMultiLangTTSWithLevel(
+ ttsEntity: MultiLangTtsEntity,
+ level: Int,
+ callBack: IMogoTTSCallback?
+ ) {
+ if (Thread.currentThread() != Looper.getMainLooper().thread) {
+ UiThreadHandler.post {
+ if (callBack != null) {
+ speakVoiceMap[ttsEntity.toString()] = callBack
+ }
+ speakMultiLangTTSWithLevel(ttsEntity, level)
+ }
+ } else {
+ if (callBack != null) {
+ speakVoiceMap[ttsEntity.toString()] = callBack
+ }
+ speakMultiLangTTSWithLevel(ttsEntity, level)
+ }
+ }
+
+ override fun stopSpeakTts(text: String?) {
+ stopTts()
+ }
+
+ override fun stopTts() {
+ if (Thread.currentThread() != Looper.getMainLooper().thread) {
+ UiThreadHandler.post {
+ realStop()
+ }
+ } else {
+ realStop()
+ }
+ }
+
+ private fun realStop() {
+ curTtsEntity?.let {
+ val string = it.toString()
+ if (speakVoiceMap.containsKey(string)) {
+ speakVoiceMap.remove(string)?.onStopTts(string)
+ }
+ curTtsEntity = null
+ }
+ curTtsContent = ""
+ curTtsLevel = -1
+ AudioTrackManager.instance?.stopPlay()
+ }
+
+ override fun speakQAndACmd(tts: String?, callback: IMogoTTSCallback?) {
+ }
+
+ override fun speakQAndACmd(
+ tts: String?,
+ okWords: Array?,
+ cancelWords: Array?,
+ callback: IMogoTTSCallback?
+ ) {
+ }
+
+ override fun registerUnWakeupCommand(
+ cmd: String?,
+ cmdWords: Array?,
+ callback: IMogoTTSCallback?
+ ) {
+ }
+
+ override fun unregisterUnWakeupCommand(cmd: String?) {
+ }
+
+ override fun unregisterUnWakeupCommand(cmd: String?, callback: IMogoTTSCallback?) {
+ }
+
+ override fun registerTtsListener(callback: IGlobalTtsCallback?) {
+ mGlobalTtsCallback = callback
+ }
+
+ override fun startAIAssist(context: Context?) {
+ }
+
+ override fun startAIAssist(context: Context?, status: Int) {
+ }
+
+ override fun breakOffSpeak() {
+ }
+
+ // 降序插入Tts(目前Level0、1可排队)
+ private fun insertTts(ttsEntity: MultiLangTtsEntity, level: Int) {
+ var index = -1
+ for (i in linkedList.indices.reversed()) {
+ val nodeLevel = linkedList[i].second
+ if (linkedList[i].first.isTimeout()) {
+ linkedList.removeAt(i)
+ } else if (level > nodeLevel) { // 只有高优先级才插入到前面,等于的情况下是插到后面
+ index = i
+ } else { // 再往前元素优先级更大,直接break
+ break
+ }
+ }
+ if (index >= 0) {
+ linkedList.add(index, Pair(ttsEntity, level))
+ } else {
+ linkedList.addLast(Pair(ttsEntity, level))
+ }
+ for (ttsPair in linkedList) {
+ d(TAG, "tts文本为:" + ttsPair.first + ",level为:" + ttsPair.second)
+ }
+ d(TAG, "===================")
+ }
+
+ private fun speakMultiLangTTSWithLevel(ttsEntity: MultiLangTtsEntity, ttsLevel: Int) {
+ ttsEntity.markTime()
+ if (ttsLevel == curTtsLevel) {
+ // 对应p3、p2级别的排队
+ if (ttsLevel == 0 || ttsLevel == 1) {
+ d(TAG, "===================")
+ d(TAG, "插入消息:$ttsEntity,level为:$ttsLevel")
+ insertTts(ttsEntity, ttsLevel)
+ return
+ } else if (ttsLevel == 2) {
+ d(TAG, "已有p2级别在播报,新内容直接丢弃!")
+ return
+ } else {
+ // 打断并合成新的
+ stopTts()
+ d(TAG, "非Level1同级别打断!")
+ }
+ } else {
+ // 将要TTS的比现在正在TTS的优先级高
+ if (ttsLevel > curTtsLevel) {
+ if (curTtsLevel >= 0) {
+ // 打断并合成高优先级的
+ stopTts()
+ }
+ d(TAG, "高优先级打断低级别的!")
+ } else {
+ if (ttsLevel == 0 || ttsLevel == 1) {
+ d(TAG, "===================")
+ d(TAG, "插入消息:$ttsEntity,level为:$ttsLevel")
+ insertTts(ttsEntity, ttsLevel)
+ } else if (ttsLevel == 2) {
+ d(TAG, "已有高级别在播报,新内容直接丢弃!")
+ }
+ return
+ }
+ }
+ curTtsLevel = ttsLevel
+ curTtsEntity = ttsEntity
+ // 合成并播放
+ d(TAG, "tts准备合成:$ttsEntity,curTtsLevel为:$curTtsLevel")
+ startSpeak(ttsEntity.ttsNext())
+ }
+
+ private fun startSpeak(langTtsEntity: LangTtsEntity?) {
+ langTtsEntity?.let {
+ curTtsContent = it.ttsContent
+ realSpeak(it.ttsContent, 2)
+ }
+ }
+
+ /**
+ * 1:中文, 2:英文, 3:法语, 5:日语, 6:俄语, 9:德语, 15:意大利语, 16:韩语, 23:西班牙语, 12:粤语, 8:印地语, 27:泰语
+ */
+ private fun realSpeak(content: String, language: Int) {
+ deleteAllOutPutDir()
+
+ // 开启会话
+ val builder = AiInput.builder()
+ builder.param("vcn", "xiaoyan")
+ builder.param("language", 1)
+ builder.param("textEncoding", "UTF-8")
+ builder.param("pitch", 50)
+ builder.param("volume", 50)
+ builder.param("speed", 50)
+ aiHandle = AiHelper.getInst().start(ABILITYID, builder.build(), null)
+ if (!aiHandle!!.isSuccess) {
+ handleErrorEvent("开启会话报错:${aiHandle!!.code}")
+ aiHandle = null
+ return
+ }
+ // 开始写入
+ val dataBuilder = AiRequest.Builder()
+ val input = AiText.get("text").data(content).valid()
+ dataBuilder.payload(input)
+ val ret = AiHelper.getInst().write(dataBuilder.build(), aiHandle)
+ if (ret != 0) {
+ handleErrorEvent("写能力输入数据失败")
+ aiHandle = null
+ }
+ }
+
+ private fun deleteAllOutPutDir() {
+ val dir = File(OUTPUT_DIR)
+ if (!dir.exists() || !dir.isDirectory || dir.listFiles() == null) return
+ for (file in dir.listFiles()!!) {
+ if (file.isFile) file.delete() // 删除所有文件
+ }
+ if (!dir.exists()) {
+ dir.mkdirs()
+ }
+ }
+
+ private fun handleCompleteEvent() {
+ curTtsLevel = -1
+ if (curTtsEntity != null) {// 多语言
+ val langTtsEntity = curTtsEntity!!.ttsNext()
+ if (langTtsEntity != null) {
+ ttsNextLanguage(langTtsEntity)
+ } else {
+ mGlobalTtsCallback?.onTtsSpeakEnd()
+ speakVoiceMap.remove(curTtsEntity.toString())?.onSpeakEnd(curTtsEntity.toString())
+ curTtsEntity = null
+ ttsNextMultiLangEntity()
+ }
+ } else {// 单语言
+ mGlobalTtsCallback?.onTtsSpeakEnd()
+ speakVoiceMap.remove(curTtsContent)?.onSpeakEnd(curTtsContent)
+ curTtsContent = ""
+ ttsNextMultiLangEntity()
+ }
+ }
+
+ private fun handleErrorEvent(error: String) {
+ curTtsLevel = -1
+ if (curTtsEntity != null) {
+ speakVoiceMap.remove(curTtsEntity.toString())
+ ?.onSpeakError(
+ curTtsEntity.toString(),
+ error
+ )
+ } else {
+ speakVoiceMap.remove(curTtsContent)?.onSpeakError(
+ curTtsContent,
+ error
+ )
+ }
+ curTtsEntity = null
+ curTtsContent = ""
+ }
+
+ private val aiRespListener = object : AiListener {
+ override fun onResult(handleID: Int, list: MutableList?, usrCxt: Any?) {
+ if (null != list && list.size > 0) {
+ val dir = File(OUTPUT_DIR)
+ var bytes: ByteArray?
+ for (i in list.indices) {
+ bytes = list[i].value ?: continue
+ d(TAG, "onResult:handleID:" + handleID + ":" + list[i].key)
+ if (!dir.exists()) {
+ dir.mkdirs()
+ }
+ FileUtils.writeFile("${OUTPUT_DIR}/OutPut_mogo.pcm", bytes)
+ }
+ }
+ }
+
+ override fun onEvent(
+ handleID: Int,
+ event: Int,
+ eventData: MutableList?,
+ usrCxt: Any?
+ ) {
+ when (event) {
+ 0 -> {
+ d(TAG, "未知错误")
+ handleErrorEvent("未知错误")
+ }
+
+ AeeEvent.AEE_EVENT_END.value -> {
+ aiHandle?.let {
+ val ret = AiHelper.getInst().end(it)
+ d(TAG, "AIKit_End:$ret")
+ }
+ onSpeakBegin()
+ AudioTrackManager.instance?.startPlay("${OUTPUT_DIR}/OutPut_mogo.pcm")
+ }
+ }
+ }
+
+ override fun onError(handleID: Int, err: Int, msg: String?, usrCxt: Any?) {
+ d(TAG, "错误码:$err,错误信息:$msg")
+ handleErrorEvent("错误码:$err,错误信息:$msg")
+ }
+ }
+
+ private fun onSpeakBegin() {
+ mGlobalTtsCallback?.onTtsSpeakStart()
+ if (Thread.currentThread() == Looper.getMainLooper().thread) {
+ curTtsEntity?.let {
+ speakVoiceMap[it.toString()]?.onSpeakStart(it.toString())
+ }
+ } else {
+ UiThreadHandler.post {
+ curTtsEntity?.let {
+ speakVoiceMap[it.toString()]?.onSpeakStart(it.toString())
+ }
+ }
+ }
+ }
+
+ private fun onCompleted() {
+ if (Thread.currentThread() == Looper.getMainLooper().thread) {
+ handleCompleteEvent()
+ } else {
+ UiThreadHandler.post {
+ handleCompleteEvent()
+ }
+ }
+ }
+
+ /**
+ * 语音合成下一个MultiLangTtsEntity
+ */
+ private fun ttsNextMultiLangEntity() {
+ var ttsPair: Pair?
+ val ttsList = linkedList
+ if (!ttsList.isEmpty()) {
+ ttsPair = ttsList.removeFirst()
+ while (ttsPair != null && ttsPair.first.isTimeout()) {
+ if (!ttsList.isEmpty()) {
+ ttsPair = ttsList.removeFirst()
+ } else {
+ ttsPair = null
+ break
+ }
+ }
+ if (ttsPair != null) {
+ i(
+ TAG,
+ "排队播放的下一条文本为:" + ttsPair.first + ",级别为:" + ttsPair.second
+ )
+ speakMultiLangTTSWithLevel(ttsPair.first, ttsPair.second)
+ } else {
+ i(TAG, "未超时的队列为空!")
+ }
+ } else {
+ i(TAG, "队列为空!")
+ }
+ }
+
+ /**
+ * 语音合成下一个MultiLangTtsEntity中的语言
+ */
+ private fun ttsNextLanguage(langTtsEntity: LangTtsEntity) {
+ startSpeak(langTtsEntity)
+ }
+}
\ No newline at end of file
diff --git a/tts/tts-iflytek-offline/src/main/java/com/mogo/tts/iflytekoffline/ParamInfo.kt b/tts/tts-iflytek-offline/src/main/java/com/mogo/tts/iflytekoffline/ParamInfo.kt
new file mode 100644
index 0000000000..a8bb5b3827
--- /dev/null
+++ b/tts/tts-iflytek-offline/src/main/java/com/mogo/tts/iflytekoffline/ParamInfo.kt
@@ -0,0 +1,19 @@
+package com.mogo.tts.iflytekoffline
+
+class ParamInfo {
+ var showName: String? = null
+
+ var value: String? = null
+
+ var value2: Int = 0
+
+ constructor(value: String?, showName: String?) {
+ this.showName = showName
+ this.value = value
+ }
+
+ constructor(value: Int, showName: String?) {
+ this.showName = showName
+ this.value2 = value
+ }
+}
\ No newline at end of file
diff --git a/tts/tts-iflytek-offline/src/main/java/com/mogo/tts/iflytekoffline/XTTSParams.kt b/tts/tts-iflytek-offline/src/main/java/com/mogo/tts/iflytekoffline/XTTSParams.kt
new file mode 100644
index 0000000000..271e02141b
--- /dev/null
+++ b/tts/tts-iflytek-offline/src/main/java/com/mogo/tts/iflytekoffline/XTTSParams.kt
@@ -0,0 +1,37 @@
+package com.mogo.tts.iflytekoffline
+
+class XTTSParams {
+ var vcn: String = "xiaoyan"
+ var language: Int = 1
+ var pitch: Int = 50
+ var speed: Int = 50
+ var volume: Int = 50
+
+ fun getVCN(): List {
+ val vcnList: MutableList = ArrayList()
+ vcnList.add(ParamInfo("xiaoyan", "xiaoyan(中文)"))
+ vcnList.add(ParamInfo("xiaofeng", "xiaofeng(中文)"))
+ vcnList.add(ParamInfo("catherine", "catherine(英文)"));
+// vcnList.add(new ParamInfo("zhongcun", "zhongcun(日语)"));
+// vcnList.add(new ParamInfo("kim", "kim(韩语)"));
+// vcnList.add(new ParamInfo("mariane", "mariane(法语)"));
+// vcnList.add(new ParamInfo("felisa", "felisa(西班牙语)"));
+// vcnList.add(new ParamInfo("keshu", "keshu(俄语)"));
+// vcnList.add(new ParamInfo("christiance", "christiance(德语)"));
+ return vcnList
+ }
+
+ fun getLanguage(): List {
+ val languageList: MutableList = ArrayList()
+ languageList.add(ParamInfo("1", "中文"))
+ languageList.add(ParamInfo("1", "中文"))
+ languageList.add(ParamInfo("2", "英文"));
+// languageList.add(new ParamInfo("5", "日语"));
+// languageList.add(new ParamInfo("16", "韩语"));
+// languageList.add(new ParamInfo("3", "法语"));
+// languageList.add(new ParamInfo("23", "西班牙语"));
+// languageList.add(new ParamInfo("6", "俄语"));
+// languageList.add(new ParamInfo("9", "德语"));
+ return languageList
+ }
+}
\ No newline at end of file
diff --git a/tts/tts-iflytek-offline/src/main/res/values/strings.xml b/tts/tts-iflytek-offline/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..0d2c4cc409
--- /dev/null
+++ b/tts/tts-iflytek-offline/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file