[ipv6][Feat]讯飞语音合成升级成离线版
This commit is contained in:
@@ -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')
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件拷贝监听
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.mogo.mgintelligent.speech"
|
||||
android:versionCode="1" >
|
||||
|
||||
<uses-sdk
|
||||
android:minSdkVersion="23"
|
||||
android:targetSdkVersion="29" />
|
||||
|
||||
</manifest>
|
||||
@@ -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
|
||||
|
||||
1
tts/tts-iflytek-offline/.gitignore
vendored
Normal file
1
tts/tts-iflytek-offline/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
||||
65
tts/tts-iflytek-offline/build.gradle
Normal file
65
tts/tts-iflytek-offline/build.gradle
Normal file
@@ -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()
|
||||
0
tts/tts-iflytek-offline/consumer-rules.pro
Normal file
0
tts/tts-iflytek-offline/consumer-rules.pro
Normal file
3
tts/tts-iflytek-offline/gradle.properties
Normal file
3
tts/tts-iflytek-offline/gradle.properties
Normal file
@@ -0,0 +1,3 @@
|
||||
GROUP=com.mogo.tts
|
||||
POM_ARTIFACT_ID=tts-iflytek-offline
|
||||
VERSION_CODE=1
|
||||
BIN
tts/tts-iflytek-offline/libs/AIKit.aar
Normal file
BIN
tts/tts-iflytek-offline/libs/AIKit.aar
Normal file
Binary file not shown.
21
tts/tts-iflytek-offline/proguard-rules.pro
vendored
Normal file
21
tts/tts-iflytek-offline/proguard-rules.pro
vendored
Normal file
@@ -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
|
||||
3
tts/tts-iflytek-offline/src/main/AndroidManifest.xml
Normal file
3
tts/tts-iflytek-offline/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.mogo.tts.iflytekoffline">
|
||||
</manifest>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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<Pair<MultiLangTtsEntity, Int>>()
|
||||
|
||||
private val speakVoiceMap by lazy {
|
||||
HashMap<String, IMogoTTSCallback>()
|
||||
}
|
||||
|
||||
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<out String>?,
|
||||
cancelWords: Array<out String>?,
|
||||
callback: IMogoTTSCallback?
|
||||
) {
|
||||
}
|
||||
|
||||
override fun registerUnWakeupCommand(
|
||||
cmd: String?,
|
||||
cmdWords: Array<out String>?,
|
||||
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<AiResponse>?, 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<AiResponse>?,
|
||||
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<MultiLangTtsEntity, Int>?
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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<ParamInfo> {
|
||||
val vcnList: MutableList<ParamInfo> = ArrayList<ParamInfo>()
|
||||
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<ParamInfo> {
|
||||
val languageList: MutableList<ParamInfo> = ArrayList<ParamInfo>()
|
||||
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
|
||||
}
|
||||
}
|
||||
4
tts/tts-iflytek-offline/src/main/res/values/strings.xml
Normal file
4
tts/tts-iflytek-offline/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
</resources>
|
||||
Reference in New Issue
Block a user