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