tts 初始化

This commit is contained in:
yangyakun
2026-04-08 18:28:27 +08:00
parent 9af378cec7
commit 76caabd295
449 changed files with 330792 additions and 2 deletions

1
libraries/mogo-tts/app/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

View File

@@ -0,0 +1,7 @@
# 将 platform.pk8 生成 platform.pem
openssl pkcs8 -inform DER -nocrypt -in ./platform.pk8 -out ./platform.pem
# platform.x509.pe m生成 platform.p12
openssl pkcs12 -export -in ./platform.x509.pem -inkey ./platform.pem -out ./platform.p12 -password pass:android -name eb5_system
# 使用keytool工具通过指令生成keystore
keytool -importkeystore -deststorepass password -destkeystore ./eb5_system.keystore -srckeystore ./platform.p12 -srcstoretype PKCS12 -srcstorepass android

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCceAWSrA1dOBze
qmXsyKYAbjZIDG1yB7EgEb5Qhjqr4rVdAJrfcUbW8iAigMfNTXvbJiQ7ioBsJrNL
E3UjpJJoIkkE3AFJPnwKzxoFyHT2mwN7YDCdkHTSQoDha60qhzQ2GVHq9ypILQmy
BLGHXhKsmMGqdz1oALnq/eVtWL7Y6NoW+aNgCZw3qDSm3+23trRKBJ4Homn8zyxU
lvLPNtZN+Qo7jY80o7qrTPUzcasncZs7pYdUrQxT/BTh20XVHiNPu+k8m6Tt+c5U
JhNQ7FNWB79pov9KoH219+ogDQmmwbSeIUAvie0RkIk6q1qRgPFS6C+FpFdTz1/B
kHHF7sgnAgEDAoIBAGhQA7cdXj4laJRxmUiFxABJeYVdnkwFIMAL1DWu0cfseOir
EepLhI9MFWxV2ojeUpIZbX0HAEgZzNy3o20YYZrBhgM9VjDUUrHfZq6Fo08SAlJA
IGkK+IwsVeudHhxaIs67i/H6HDAeBnaty6+UDHMQgRxPfkVV0UdT7kjl1JCaMTTc
hXxr+oGsH5d5EHCPBfEU4v8BqOBSEgkUFJ1Y6Y5AQqbSe2nSZ6bKZpE8YY27Ipvq
BrZEN0j7SKcHKuJDm81DGfLrh3vOmW6U0ZWy7o9qv7INYXNDvtKUMQigXYYANKUp
KvveUSgKJ+ixfFP8Ye92j8mp+d/+16i4Wa7Wu5sCgYEAyCd3wu4wsRAu/8q1abL1
4/V8M95+dlfhJBl3H9VwUbsH+CP2VRQ9KYkaYdPwOb8hz1h8gqr74ASbEvz+ixMH
zI6zel07VDCew+6OUZOBiOxGc8p7SpZuCeDsL+GEZxkOMQ7PXUpDI/p8nx84eEJU
ZrHc2kv1VcpdAXLhtrpiicUCgYEAyCA3GDc282T3BXj061vqR2rpFaqhdlRy7S2B
tA3BJj2Qrh1bgQGq9agC+YWm3xO3F/lJ+phK2WHoFOpR+m6w7Q0jx/L/BYOlJD+4
h1zeZJKhLBQToodM1ZktcLGYFkSzNJeUImt5T1sErj5op9Ex97nZfYswCrl0GtGa
NIVKJPsCgYEAhW+lLJ7LILV0qocjm8yj7U5SzT7++Y/rbWZPao5K4SdapW1O42LT
cQYRlo1K0SoWijr9rHH9QAMSDKipsgyv3bR3pujSOCBp1/Re4Q0BBfLZoob83GRJ
W+tIH+utmhC0ILSKPjGCF/xTFL96+tbi7yE95t1OOTGTVkyWedGXBoMCgYEAhWrP
ZXokokNPWPtN8j1G2kdGDnHA+Y2h83OrzV6AxCkLHr49AKvHTnAB+65vP2J6D/uG
pxAx5kFFY0bhUZ8gngjChUyqA60YwtUlr5M+7bcWHWK3wa+IjmYeSyEQDth3eGUN
bEemNOdYdCmbGot2pSaQ/lzKsdD4EeEReFjcGKcCgYA+JeOt5WFENpv4LT+7P+j1
k3xvOZ9sJGuXRXk9HXzsJvRCnc5oScwqku6i5HjzG8gyNVZg1sQGbVbWWmcNtaS8
I3XalYAHYQyb8SGxlQP4ctKAN4j2Hbk1OHAMW84dfAQYQwFcBdaJTtMXQlbbX5Rh
x90wE4qFuIapx6IKOmDxRw==
-----END PRIVATE KEY-----

Binary file not shown.

View File

@@ -0,0 +1,27 @@
-----BEGIN CERTIFICATE-----
MIIEqDCCA5CgAwIBAgIJALOZgIbQVs/6MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYD
VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4g
VmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UE
AxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAe
Fw0wODA0MTUyMjQwNTBaFw0zNTA5MDEyMjQwNTBaMIGUMQswCQYDVQQGEwJVUzET
MBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4G
A1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9p
ZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZI
hvcNAQEBBQADggENADCCAQgCggEBAJx4BZKsDV04HN6qZezIpgBuNkgMbXIHsSAR
vlCGOqvitV0Amt9xRtbyICKAx81Ne9smJDuKgGwms0sTdSOkkmgiSQTcAUk+fArP
GgXIdPabA3tgMJ2QdNJCgOFrrSqHNDYZUer3KkgtCbIEsYdeEqyYwap3PWgAuer9
5W1Yvtjo2hb5o2AJnDeoNKbf7be2tEoEngeiafzPLFSW8s821k35CjuNjzSjuqtM
9TNxqydxmzulh1StDFP8FOHbRdUeI0+76TybpO35zlQmE1DsU1YHv2mi/0qgfbX3
6iANCabBtJ4hQC+J7RGQiTqrWpGA8VLoL4WkV1PPX8GQccXuyCcCAQOjgfwwgfkw
HQYDVR0OBBYEFE/koLPdnLop9x1yh8Tnw48ghsKZMIHJBgNVHSMEgcEwgb6AFE/k
oLPdnLop9x1yh8Tnw48ghsKZoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UE
CBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMH
QW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAG
CSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJALOZgIbQVs/6MAwGA1Ud
EwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBAFclUbjZOh9z3g9tRp+G2tZwFAAp
PIigzXzXeLc9r8wZf6t25iEuVsHHYc/EL9cz3lLFCuCIFM78CjtaGkNGBU2Cnx2C
tCsgSL+ItdFJKe+F9g7dEtctVWV+IuPoXQTIMdYT0Zk4u4mCJH+jISVroS0dao+S
6h2xw3Mxe6DAN/DRr/ZFrvIkl5+6bnoUvAJccbmBOM7z3fwFlhfPJIRc97QNY4L3
J17XOElatuWTG5QhdlxJG3L7aOCA29tYwgKdNHyLMozkPvaosVUz7fvpib1qSN1L
IC7alMarjdW4OZID2q4u1EYjLk/pvZYTlMYwDlE448/Shebk5INTjLixs1c=
-----END CERTIFICATE-----

View File

@@ -0,0 +1,128 @@
import java.text.SimpleDateFormat
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
android {
compileSdkVersion rootProject.ext.android.compileSdkVersion
signingConfigs {
releaseEB5 {
keyAlias = 'eb5_system'
storeFile file('./EB5/eb5_system.keystore')
storePassword 'password'
keyPassword 'password'
v1SigningEnabled true
v2SigningEnabled true
}
}
defaultConfig {
applicationId "com.mogo.tts"
minSdkVersion rootProject.ext.android.minSdkVersion
targetSdkVersion rootProject.ext.android.targetSdkVersion
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
debug {
debuggable true
minifyEnabled false
zipAlignEnabled true
shrinkResources false
signingConfig signingConfigs.releaseEB5
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
release {
debuggable true
minifyEnabled false
zipAlignEnabled true
shrinkResources false
signingConfig signingConfigs.releaseEB5
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
lintOptions {
abortOnError false
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.0'
implementation 'androidx.annotation:annotation:1.2.0'
implementation 'androidx.core:core-ktx:1.5.0'
implementation 'com.blankj:utilcodex:1.30.0'
implementation "com.bytedance.boost_multidex:boost_multidex:1.0.1"
implementation(project(":libraries:mogo-tts:tts-iflytek-offline"))
implementation(project(":libraries:mogo-tts:tts-base"))
// ======================集成 Bugly================================================================
implementation 'com.tencent.bugly:crashreport:4.1.9.2'
// implementation 'androidx.activity:activity:1.13.0'
// ======================集成 Bugly================================================================
}
//
//
//// 打包出来的应用文件名称
//android.applicationVariants.all { variant ->
// def buildTime = new Date().format("yyyyMMdd", TimeZone.getTimeZone("GMT+08:00"))
// def flavor = variant.productFlavors.collect { it.name }.join('-')
//
// variant.outputs.all { output ->
// outputFileName = ["MoGoTTS",
// "v${variant.versionName}",
// getWorkingBranchName(),
// getWorkingBranchHash(),
// buildTime,
// flavor.length() > 0 ? "[${flavor}]" : "",
// variant.buildType.name].findAll { it.length() > 0 }.join('_') << ".apk"
// }
//}
//
//// 获取当前分支名称
//def getWorkingBranchName() {
// def workingBranchName = ""
// def proc = "git rev-parse --abbrev-ref HEAD".execute()
// proc.in.eachLine { line -> workingBranchName = line }
// proc.err.eachLine { line -> println line }
// proc.waitFor()
// println "Working branch name: " + workingBranchName
// return workingBranchName
//}
//
//// 获取当前分支hash
//def getWorkingBranchHash() {
// def workingBranchHash = ""
// def proc = "git log -n1 --format=format:%h".execute()
// proc.in.eachLine { line -> workingBranchHash = line }
// proc.err.eachLine { line -> println line }
// proc.waitFor()
// println "Working branch hash: " + workingBranchHash
// return workingBranchHash
//}
//
//// 当前构建时间
//static def getBuildTime() {
// def buildTimeFormat = "yyyy-MM-dd HH:mm:ss"
// //设置时间格式
// SimpleDateFormat formatter = new SimpleDateFormat(buildTimeFormat, Locale.getDefault())
// //获取当前时间
// Date curDate = new Date(System.currentTimeMillis())
// def buildTime = formatter.format(curDate)
// return "\"${buildTime}\""
//}

View File

@@ -0,0 +1,33 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /projects/android-sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# 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 *;
#}
# This renames the file name on stack traces from "UnknownSource" to "SourceFile"
-renamesourcefileattribute SourceFile
# This values are kept on stack traces to debug any issue easily
-keepattributes SourceFile,LineNumberTable
# Rules recommended for Huawei PushKit
-ignorewarnings
-keepattributes *Annotation*
-keepattributes Exceptions
-keepattributes InnerClasses
-keepattributes Signature
-keepattributes SourceFile,LineNumberTable
-keep class com.hianalytics.android.**{*;}
-keep class com.huawei.updatesdk.**{*;}
-keep class com.huawei.hms.**{*;}

View File

@@ -0,0 +1,6 @@
### Sample Android app for demo
This is an example app that sends data to a remote Countly server.
In order to build and run this application, just open up the project in Android Studio and hit run with an appropriate application.

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.mogo.tts">
<!-- 允许后台启动服务 -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.RESTART_PACKAGES" /> <!-- 系统级权限:绕过后台服务限制 -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <!-- 关键:允许后台启动 Service & 前台服务 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_*" /> <!-- 系统签名专用:豁免后台限制 -->
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<application
android:name=".MoGoApplication"
android:allowBackup="false"
android:icon="@mipmap/icon"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:persistent="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:configChanges="orientation|screenSize"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,28 @@
package com.mogo.tts
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.AppCompatButton
import androidx.appcompat.widget.AppCompatTextView
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.mogo.tts.common.TtsManager
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
findViewById<AppCompatButton>(R.id.actv_test_tts1).setOnClickListener {
TtsManager.speakTTSVoice("此时每个单兵要负责自己的一个视线防区")
}
findViewById<AppCompatButton>(R.id.actv_test_tts2).setOnClickListener {
TtsManager.stopTTS()
}
}
}

View File

@@ -0,0 +1,87 @@
package com.mogo.tts
import android.app.Application
import android.content.Context
import android.util.Log
import com.blankj.utilcode.util.DeviceUtils
import com.blankj.utilcode.util.FileIOUtils
import com.blankj.utilcode.util.PathUtils
import com.blankj.utilcode.util.TimeUtils
import com.bytedance.boost_multidex.BoostMultiDex
import com.mogo.tts.common.TtsManager
import com.mogo.tts.iflytek.offline.IFlyTekOfflineTts
import com.mogo.tts.utils.PhoneUtilsExtend
import com.tencent.bugly.crashreport.CrashReport
import com.tencent.bugly.crashreport.CrashReport.CrashHandleCallback
import com.tencent.bugly.crashreport.CrashReport.UserStrategy
class MoGoApplication : Application() {
val TAG = "MoGoApplication"
override fun attachBaseContext(base: Context) {
super.attachBaseContext(base)
Log.d(TAG, "--->>>MoGo APP attachBaseContext")
BoostMultiDex.install(base)
}
override fun onCreate() {
super.onCreate()
Log.d(TAG, "--->>>MoGo APP 应用商店启动onCreate")
initBugly()
initTts()
}
private fun initTts() {
TtsManager.init(this)
}
private fun initBugly() {
val strategy = UserStrategy(this)
// 通过UserStrategy设置
strategy.deviceID = PhoneUtilsExtend.getDevicesSn()
// 通过UserStrategy设置
strategy.deviceModel = DeviceUtils.getModel()
// 最新版SDK支持trace文件采集和anr过程中的主线程堆栈信息采集由于抓取堆栈的系统接口 Thread.getStackTrace 可能造成crash建议只对少量用户开启
// 这里只对Debug模式下的开启
if (BuildConfig.DEBUG) {
strategy.appChannel = "MoGo_Debug"
// 设置anr时是否获取系统trace文件默认为false
strategy.isEnableCatchAnrTrace = true
// 设置是否获取anr过程中的主线程堆栈默认为true
strategy.isEnableRecordAnrMainStack = false
} else {
strategy.appChannel = "MoGo_Debug"
}
// 设置Crash回调,将日志信息本地存储
strategy.setCrashHandleCallback(object : CrashHandleCallback() {
override fun onCrashHandleStart(
crashType: Int, errorType: String,
errorMessage: String, errorStack: String
): Map<String, String> {
val map = LinkedHashMap<String, String>()
map["device_sn"] = PhoneUtilsExtend.getDevicesSn()
FileIOUtils.writeFileFromString(
PathUtils.getExternalAppFilesPath() + "/crash/" + TimeUtils.getNowString() + ".log",
"crashType = $crashType \nerrorType = $errorType \nerrorMessage = $errorMessage \nerrorStack = $errorStack"
)
return map
}
override fun onCrashHandleStart2GetExtraDatas(
crashType: Int, errorType: String,
errorMessage: String, errorStack: String
): ByteArray? {
return try {
"Extra data.".toByteArray(charset("UTF-8"))
} catch (e: Exception) {
null
}
}
})
CrashReport.initCrashReport(this, strategy)
}
}

View File

@@ -0,0 +1,29 @@
package com.mogo.tts.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Handler
import android.os.Looper
/**
* 监听系统启动
*/
class BootReceiver : BroadcastReceiver() {
private val TAG = "BootReceiver"
private val TARGET_PKG: String = "com.mogo.launcher.f"
private val mHandler = Handler(Looper.getMainLooper())
private var mRetryCount = 0
private val MAX_RETRY: Int = 20 // 最多重试 20 次
private val DELAY: Int = 500 // 每 500ms 查一次
override fun onReceive(context: Context, intent: Intent) {
if (Intent.ACTION_BOOT_COMPLETED == intent.action||Intent.ACTION_LOCKED_BOOT_COMPLETED == intent.action) { //接收BOOT_COMPLETED广播
}
}
}

View File

@@ -0,0 +1,13 @@
package com.mogo.tts.utils
import android.os.Build
/**
* 对PhoneUtils扩展一些方法获取系统的参数
*/
object PhoneUtilsExtend {
fun getDevicesSn(): String {
return Build.getSerial()
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 B

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/actv_test_tts1"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:text="开始播放"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/actv_test_tts2"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@+id/actv_test_tts1"
android:text="停止播放"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</androidx.constraintlayout.widget.ConstraintLayout>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@@ -0,0 +1,6 @@
<resources>
<!-- Example customization of dimensions originally defined in res/values/dimens.xml
(such as screen margins) for screens with more than 820dp of available width. This
would include 7" and 10" devices in landscape (~960dp and ~1280dp respectively). -->
<dimen name="activity_horizontal_margin">64dp</dimen>
</resources>

View File

@@ -0,0 +1,6 @@
<resources>
<dimen name="fab_margin">16dp</dimen>
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
</resources>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">TTS Demo</string>
<string name="deep_link">Deep Link Test</string>
<string name="countly_hannel_name">General Notifications</string>
<string name="countly_channel_description"><![CDATA[News & announcements]]></string>
<string name="title_activity_another">AnotherActivity</string>
<string name="another_activity_text">Another Activity</string>
</resources>

View File

@@ -0,0 +1,17 @@
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
</style>
<style name="AppTheme.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
<style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true" />
</network-security-config>

View File

View File

@@ -0,0 +1 @@
/build

View File

@@ -0,0 +1,67 @@
### 换肤SDK
#### 使用方式
```java
public class MoGoApplication extends MultiDexApplication {
private static final String TAG = "MoGoApplication";
@Override
public void onCreate() {
super.onCreate();
// 初始化动态换肤SDK
SkinManager.init(this);
}
}
```
##### 基础控件0改动完成资源替换只需要保持使用的「资源文件drawable、string、color等」皮肤包与APP中保持一致即可
TextView 控件的静态文字一定要用string.xml 的方式引用文字显示
例如:
```xml
<TextView
android:id="@+id/tvDevicesId"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:text="@string/devicesId"
android:textColor="@color/colorAccent"
android:textSize="20dp" />
```
##### 如果代码中动态控制了 ImageView 的 src、background 一定要在 XML 中替换成 SkinImageView.java
##### 代码中动态获取资源设置:图片、文字、颜色
```java
// 获取颜色
SkinResources.getInstance().getColor(resId);
// 获取图片
SkinResources.getInstance().getDrawable(resId);
// 获取文字
SkinResources.getInstance().getString(resId);
// 获取指定的资源id
SkinResources.getInstance().getIdentifier(resId);
// 获取指定的Raw资源 InputStream
SkinResources.getInstance().getRawInputStream(resId);
// 获取指定的Raw资源的byte[]
SkinResources.getInstance().getRawResourceBytes(resId);
```
#### 手动控制切换皮肤
```java
Skin skin = new Skin(
// 皮肤包 MD5 用于文件损坏校验的
"d5493244467d3970834e42dc1a6f07c9",
// 皮肤文件名称
"app-skin-debug.skin",
// 外网可访问的文件服务器地址
"https://carlife-static-1255510688.cos.ap-beijing.myqcloud.com/MoGoEagleEye/app-skin-debug.skin");
// 换肤
SkinManager.getInstance().selectSkin(this, skin);
// 还原
SkinManager.getInstance().loadSkin("");
```

View File

@@ -0,0 +1,39 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion rootProject.ext.android.compileSdkVersion
defaultConfig {
minSdkVersion rootProject.ext.android.minSdkVersion
targetSdkVersion rootProject.ext.android.targetSdkVersion
versionCode 1
versionName "${MOGO_REALTIME_VERSION}"
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation rootProject.ext.dependencies.androidxappcompat
api "com.elegant.utils:common-utils:1.0.58"
}
apply from: new File(rootProject.rootDir, "gradle/upload.gradle").toString()

View File

@@ -0,0 +1,4 @@
GROUP=com.mogo.cloud
POM_ARTIFACT_ID=tts-base
VERSION_CODE=1
VERSION_NAME=1.0.1-SNAPSHOT

View 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

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.mogo.tts.common">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
</manifest>

View File

@@ -0,0 +1,7 @@
package com.mogo.tts.common
interface IGlobalTtsCallback {
fun onTtsSpeakStart()
fun onTtsSpeakEnd()
}

View File

@@ -0,0 +1,33 @@
package com.mogo.tts.common;
import android.content.Context;
public
/**
* @author congtaowang
* @since 2020/10/12
*
* 语音抽象
*/
interface IMogoTTS {
void initTts(Context context);
/**
* 释放资源
*/
void release();
void speakTTSVoice(String tts);
void speakTTSVoice(String tts, IMogoTTSCallback callBack);
void speakTTSVoice(LangTtsEntity ttsEntity, IMogoTTSCallback callBack);
void stopTts();
void registerTtsListener(IGlobalTtsCallback callback);
default void clearTTSCallback(String tts) {
}
}

View File

@@ -0,0 +1,90 @@
package com.mogo.tts.common;
public
/**
* @author congtaowang
* @since 2020/10/12
*
* 描述
*/
interface IMogoTTSCallback {
/**
* 新SDK接口
*
* @param ttsId
* @param tts
*/
@Deprecated
default void onTTSStart( String ttsId, String tts ) {
}
/**
* 新SDK接口
*
* @param ttsId
* @param tts
*/
@Deprecated
default void onTTSEnd( String ttsId, String tts ) {
}
/**
* 新SDK接口
*
* @param ttsId
* @param tts
*/
@Deprecated
default void onTTSError( String ttsId, String tts ) {
}
/**
* 免唤醒命令响应回调
*
* @param cmd
*/
default void onCmdSelected( String cmd ) {
}
/**
* 语音播报临时免唤醒“确定”命令
*
* @param speakText 播报内容
*/
default void onCmdAction( String speakText ) {
}
/**
* 语音播报临时免唤醒“取消”命令
*
* @param speakText 播报内容
*/
default void onCmdCancel( String speakText ) {
}
default void onSpeakStart( String speakText ) {
}
/**
* 语音播报完毕
*
* @param speakText 播报内容
*/
default void onSpeakEnd( String speakText ) {
}
default void onStopTts( String speakText ) {
}
default void onSpeakError( String speakText, String errorMsg) {
}
/**
* 语音播报完临时命令选择超时
*
* @param speakText 播报内容
*/
default void onSpeakSelectTimeOut( String speakText ) {
}
}

View File

@@ -0,0 +1,7 @@
package com.mogo.tts.common
enum class LanguageType(val langName: String) {
CHINESE("chinese"),
ENGLISH("english"),
KOREAN("korean")
}

View File

@@ -0,0 +1,55 @@
package com.mogo.tts.common
data class MultiLangTtsEntity(
private var ttsList: List<LangTtsEntity>
) {
companion object {
private const val TIMEOUT_MILLIS = 60000
}
private val stringBuffer by lazy {
StringBuffer()
}
private var ttsIndex = 0
private var timeStamp: Long = 0
fun ttsNext(): LangTtsEntity? {
return if (ttsIndex in ttsList.indices) {
ttsList[ttsIndex++]
} else {
null
}
}
fun markTime() {
timeStamp = System.currentTimeMillis()
}
fun isTimeout():Boolean {
return timeStamp > 0 && System.currentTimeMillis() - timeStamp >= TIMEOUT_MILLIS
}
override fun toString(): String {
return stringBuffer.let {
it.setLength(0)
ttsList.forEachIndexed { index, langTtsEntity ->
if (index != ttsList.size - 1) {
it.append("${langTtsEntity};")
} else {
it.append(langTtsEntity)
}
}
it.toString()
}
}
}
data class LangTtsEntity(
var ttsContent: String,
var language: LanguageType
) {
override fun toString(): String {
return "ttsContent is:$ttsContent,language is:${language.langName}"
}
}

View File

@@ -0,0 +1,32 @@
package com.mogo.tts.common;
public
/**
* @author congtaowang
* @since 2020/10/12
*
* 语音打算类型
*/
enum PreemptType {
/**
* 不打断
*/
PREEMPT_TYPE_NONE,
/**
* 立即打断取消当前的tts插队播放
*/
PREEMPT_TYPE_IMMEDIATELY,
/**
* 下一个插入不取消当前的tts插队下一个播放
*/
PREEMPT_TYPE_NEXT,
/**
* 清空队列
*/
PREEMPT_TYPE_FLUSH,
/**
* 立即打断不取消当前tts
*/
PREEMPT_TYPE_IMMEDIATELY_WITHOUT_CANCEL
}

View File

@@ -0,0 +1,41 @@
package com.mogo.tts.common
import android.content.Context
import com.mogo.tts.common.log.TtsLogManager
object TtsManager {
private const val TAG = "TtsManager"
private var mTTS: IMogoTTS? = null
fun init(context: Context){
try {
// 暂时换成反射,解决死锁问题
var clazz1: Class<*>? = null
try {
clazz1 = Class.forName("com.mogo.tts.iflytek.offline.IFlyTekOfflineTts")
} catch (ignored: Exception) {
}
if (clazz1 != null) {
mTTS = clazz1.getConstructor().newInstance() as IMogoTTS
}
mTTS?.initTts(context)
} catch (e: Exception) {
e.printStackTrace()
TtsLogManager.d(TAG,"TTS 模块初始化异常")
}
}
fun speakTTSVoice(tts:String){
mTTS?.speakTTSVoice(tts)
}
fun stopTTS(){
mTTS?.stopTts()
}
}

View File

@@ -0,0 +1,5 @@
package com.mogo.tts.common.log;
public enum LogLevel {
info,warn,debug,error
}

View File

@@ -0,0 +1,6 @@
package com.mogo.tts.common.log;
public interface LogListener {
void writeLog(LogLevel logLevel,String tag,String message);
}

View File

@@ -0,0 +1,51 @@
package com.mogo.tts.common.log
import android.util.Log
object TtsLogManager {
private var isDebug = true
private var logListener:LogListener? = null
fun setDebug(isDebug:Boolean){
this.isDebug = isDebug
}
fun setLogListener(logListener: LogListener){
this.logListener = logListener
}
@JvmStatic
fun d(tag: String, message: String) {
if(isDebug) {
Log.d(tag, message);
}
logListener?.writeLog(LogLevel.debug,tag,message)
}
@JvmStatic
fun i(tag: String, message: String) {
if(isDebug) {
Log.i(tag, message)
}
logListener?.writeLog(LogLevel.info,tag,message)
}
@JvmStatic
fun w(tag: String, message: String) {
if(isDebug) {
Log.w(tag, message)
}
logListener?.writeLog(LogLevel.warn,tag,message)
}
@JvmStatic
fun e(tag: String, message: String) {
if(isDebug) {
Log.e(tag, message)
}
logListener?.writeLog(LogLevel.error,tag,message)
}
}

View File

@@ -0,0 +1,71 @@
package com.mogo.tts.common.utils;
import android.content.Context;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class FileUtils {
public static void copyAssetsToLocal(Context context, String assetDir, String localDir) throws IOException {
// 获取assets目录下的所有文件和目录
String[] files = context.getAssets().list(assetDir);
if (files == null || files.length == 0) {
return;
}
// 创建本地目标目录
File localDirFile = new File(localDir);
if (!localDirFile.exists()) {
localDirFile.mkdirs();
}
for (String fileName : files) {
String assetPath = assetDir + File.separator + fileName;
String localPath = localDir + File.separator + fileName;
// 如果是文件夹,递归调用
if (isDirectory(context, assetPath)) {
copyAssetsToLocal(context, assetPath, localPath);
} else {
try (InputStream in = context.getAssets().open(assetPath);
FileOutputStream out = new FileOutputStream(localPath)) {
byte[] buffer = new byte[1024];
int read;
while ((read = in.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
}
}
}
}
// 判断 assets 中的路径是否是文件夹
public static boolean isDirectory(Context context, String path) throws IOException {
String[] files = context.getAssets().list(path);
return files != null && files.length > 0;
}
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();
}
}
}

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="skinTypeface" format="string" />
<!--防止大面积误伤原生View这里做一个手动开启换肤的开关来控制是否要在这个控件上使用-->
<attr name="isUseSkin" format="boolean" />
</resources>

View File

@@ -0,0 +1,3 @@
<resources>
<string name="app_name">skin-core2</string>
</resources>

View File

@@ -0,0 +1 @@
/build

View File

@@ -0,0 +1,67 @@
### 换肤SDK
#### 使用方式
```java
public class MoGoApplication extends MultiDexApplication {
private static final String TAG = "MoGoApplication";
@Override
public void onCreate() {
super.onCreate();
// 初始化动态换肤SDK
SkinManager.init(this);
}
}
```
##### 基础控件0改动完成资源替换只需要保持使用的「资源文件drawable、string、color等」皮肤包与APP中保持一致即可
TextView 控件的静态文字一定要用string.xml 的方式引用文字显示
例如:
```xml
<TextView
android:id="@+id/tvDevicesId"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:text="@string/devicesId"
android:textColor="@color/colorAccent"
android:textSize="20dp" />
```
##### 如果代码中动态控制了 ImageView 的 src、background 一定要在 XML 中替换成 SkinImageView.java
##### 代码中动态获取资源设置:图片、文字、颜色
```java
// 获取颜色
SkinResources.getInstance().getColor(resId);
// 获取图片
SkinResources.getInstance().getDrawable(resId);
// 获取文字
SkinResources.getInstance().getString(resId);
// 获取指定的资源id
SkinResources.getInstance().getIdentifier(resId);
// 获取指定的Raw资源 InputStream
SkinResources.getInstance().getRawInputStream(resId);
// 获取指定的Raw资源的byte[]
SkinResources.getInstance().getRawResourceBytes(resId);
```
#### 手动控制切换皮肤
```java
Skin skin = new Skin(
// 皮肤包 MD5 用于文件损坏校验的
"d5493244467d3970834e42dc1a6f07c9",
// 皮肤文件名称
"app-skin-debug.skin",
// 外网可访问的文件服务器地址
"https://carlife-static-1255510688.cos.ap-beijing.myqcloud.com/MoGoEagleEye/app-skin-debug.skin");
// 换肤
SkinManager.getInstance().selectSkin(this, skin);
// 还原
SkinManager.getInstance().loadSkin("");
```

View File

@@ -0,0 +1,39 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion rootProject.ext.android.compileSdkVersion
defaultConfig {
minSdkVersion rootProject.ext.android.minSdkVersion
targetSdkVersion rootProject.ext.android.targetSdkVersion
versionCode 1
versionName "${MOGO_REALTIME_VERSION}"
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation project(":libraries:mogo-tts:tts-base")
implementation files('libs/AIKit.aar')
implementation rootProject.ext.dependencies.androidxappcompat
}
apply from: new File(rootProject.rootDir, "gradle/upload.gradle").toString()

View File

@@ -0,0 +1,4 @@
GROUP=com.mogo.cloud
POM_ARTIFACT_ID=tts-iflytek-offline
VERSION_CODE=1
VERSION_NAME=1.0.1-SNAPSHOT

Binary file not shown.

View 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

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.mogo.tts.iflytek.offline">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
</manifest>

View File

@@ -0,0 +1,229 @@
package com.mogo.tts.iflytek.offline
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()
}
}

View File

@@ -0,0 +1,368 @@
package com.mogo.tts.iflytek.offline
import android.content.Context
import android.os.Looper
import androidx.annotation.Keep
import com.elegant.utils.ResourcesHelper
import com.elegant.utils.ThreadPoolService
import com.elegant.utils.UiThreadHandler
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.tts.common.IGlobalTtsCallback
import com.mogo.tts.common.IMogoTTS
import com.mogo.tts.common.IMogoTTSCallback
import com.mogo.tts.common.LangTtsEntity
import com.mogo.tts.common.log.TtsLogManager
import com.mogo.tts.common.LanguageType
import com.mogo.tts.common.utils.FileUtils
import java.io.File
@Keep
class IFlyTekOfflineTts : IMogoTTS {
companion object {
const val TAG = "IFlyTekTts"
}
private var context: Context? = null
private var aiHandle: AiHandle? = null
private var OUTPUT_DIR :String = ""
private val ABILITYID by lazy {
"e2e44feff"
}
// 由于主动打断不会有回调事件所以主动打断时清掉map中被打断的text和callback
@Volatile
private var curTtsContent = ""
@Volatile
private var curTtsEntity: LangTtsEntity? = null
private val speakVoiceMap by lazy {
HashMap<String, IMogoTTSCallback>()
}
private var mGlobalTtsCallback: IGlobalTtsCallback? = null
override fun initTts(context: Context) {
this.context = context
initSDK()
}
private fun initSDK() {
context?.let {
val workPath = it.filesDir.absolutePath+File.separator+"aikit"+File.separator+"xtts"
OUTPUT_DIR = workPath+File.separator+"output"
val file = File("$workPath/e3fe94474_1.0.0_xTTS_CnCn_xiaoyan_2018_arm.irf")
if (!file.exists() || file.length() == 0L) {
ThreadPoolService.execute {
FileUtils.copyAssetsToLocal(context, "xTTS", workPath)
initEngine(workPath)
}
} else {
ThreadPoolService.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("e2e44feff;e867a88f2")
.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授权成功
TtsLogManager.d(TAG, "科大讯飞离线语音合成授权成功!")
} else {
// SDK授权失败授权码为code
TtsLogManager.d(TAG, "科大讯飞离线语音合成授权失败码:$code")
}
}
else -> {
// SDK状态为type, code
TtsLogManager.d(TAG, "type:$type, code:$code")
}
}
}
private val completeListener = object : AudioTrackManager.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 speakTTSVoice(tts: String?) {
speakTTSVoice(tts, null)
}
override fun speakTTSVoice(tts: String?, callBack: IMogoTTSCallback?) {
if (tts.isNullOrEmpty()) return
speakTTSVoice(LangTtsEntity(tts, LanguageType.CHINESE), callBack)
}
override fun speakTTSVoice(ttsEntity: LangTtsEntity, callBack: IMogoTTSCallback?) {
if (Thread.currentThread() != Looper.getMainLooper().thread) {
UiThreadHandler.post {
if (callBack != null) {
speakVoiceMap[ttsEntity.toString()] = callBack
}
speakMultiLangTTS(ttsEntity)
}
} else {
if (callBack != null) {
speakVoiceMap[ttsEntity.toString()] = callBack
}
speakMultiLangTTS(ttsEntity)
}
}
override fun stopTts() {
if (Thread.currentThread() != Looper.getMainLooper().thread) {
UiThreadHandler.post {
realStop()
}
} else {
realStop()
}
}
private fun realStop() {
TtsLogManager.d(TAG,"停止tts")
curTtsEntity?.let {
val string = it.toString()
if (speakVoiceMap.containsKey(string)) {
speakVoiceMap.remove(string)?.onStopTts(string)
}
curTtsEntity = null
}
curTtsContent = ""
AudioTrackManager.instance?.stopPlay()
if (aiHandle == null || aiHandle?.isSuccess == false) {
return
}
val end = AiHelper.getInst().end(aiHandle)
aiHandle = null
TtsLogManager.d(TAG,"停止tts:${end}")
}
override fun registerTtsListener(callback: IGlobalTtsCallback?) {
mGlobalTtsCallback = callback
}
private fun speakMultiLangTTS(ttsEntity: LangTtsEntity) {
curTtsEntity = ttsEntity
// 合成并播放
TtsLogManager.d(TAG, "tts准备合成$ttsEntity")
stopTts()
startSpeak(ttsEntity)
}
private fun startSpeak(langTtsEntity: LangTtsEntity?) {
langTtsEntity?.let {
curTtsContent = it.ttsContent
realSpeak(it.ttsContent)
}
}
/**
* 1:中文, 2:英文, 3:法语, 5:日语, 6:俄语, 9:德语, 15:意大利语, 16:韩语, 23:西班牙语, 12:粤语, 8:印地语, 27:泰语
*/
private fun realSpeak(content: String) {
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}")
val errorInfo = ResourcesHelper.getString(context, R.string.module_tts_start_ai_handle_error)
handleErrorEvent("${errorInfo}${aiHandle!!.code}")
AiHelper.getInst().end(aiHandle)
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("写能力输入数据失败")
val errorInfo = ResourcesHelper.getString(context, R.string.module_tts_write_ai_handle_error)
handleErrorEvent(errorInfo)
AiHelper.getInst().end(aiHandle)
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() {
mGlobalTtsCallback?.onTtsSpeakEnd()
speakVoiceMap.remove(curTtsContent)?.onSpeakEnd(curTtsContent)
curTtsContent = ""
}
private fun handleErrorEvent(error: String) {
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
TtsLogManager.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) {
AeeEvent.AEE_EVENT_UNKNOWN.value -> {
TtsLogManager.d(TAG, "未知错误")
// handleErrorEvent("未知错误")
aiHandle?.let {
val ret = AiHelper.getInst().end(it)
aiHandle = null
TtsLogManager.d(TAG, "AIKit_End$ret")
}
val errorInfo = ResourcesHelper.getString(context, R.string.module_tts_unknown_error)
handleErrorEvent(errorInfo)
}
AeeEvent.AEE_EVENT_TIMEOUT.value->{
TtsLogManager.d(TAG, "超时错误")
// handleErrorEvent("未知错误")
aiHandle?.let {
val ret = AiHelper.getInst().end(it)
aiHandle = null
TtsLogManager.d(TAG, "AIKit_End$ret")
}
val errorInfo = ResourcesHelper.getString(context, R.string.module_tts_unknown_error)
handleErrorEvent(errorInfo)
}
AeeEvent.AEE_EVENT_START.value->{
}
AeeEvent.AEE_EVENT_END.value -> {
aiHandle?.let {
val ret = AiHelper.getInst().end(it)
aiHandle = null
TtsLogManager.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?) {
TtsLogManager.d(TAG, "错误码:$err,错误信息:$msg")
// handleErrorEvent("错误码:$err,错误信息:$msg")
val errorInfo = ResourcesHelper.getResources(context)
.getString(R.string.module_tts_ai_handle_error_code, err, msg)
handleErrorEvent(errorInfo)
}
}
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()
}
}
}
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="module_tts_start_ai_handle_error">Failed to start session:</string>
<string name="module_tts_write_ai_handle_error">Failed to write capability input data</string>
<string name="module_tts_ai_handle_error_code">Error code: %d, Error message: %s</string>
<string name="module_tts_unknown_error">Unknown error</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="module_tts_start_ai_handle_error">开启会话报错:</string>
<string name="module_tts_write_ai_handle_error">写能力输入数据失败</string>
<string name="module_tts_ai_handle_error_code">错误码:%d错误信息%s</string>
<string name="module_tts_unknown_error">未知错误</string>
</resources>

View File

@@ -0,0 +1 @@
/build

View File

@@ -0,0 +1,67 @@
### 换肤SDK
#### 使用方式
```java
public class MoGoApplication extends MultiDexApplication {
private static final String TAG = "MoGoApplication";
@Override
public void onCreate() {
super.onCreate();
// 初始化动态换肤SDK
SkinManager.init(this);
}
}
```
##### 基础控件0改动完成资源替换只需要保持使用的「资源文件drawable、string、color等」皮肤包与APP中保持一致即可
TextView 控件的静态文字一定要用string.xml 的方式引用文字显示
例如:
```xml
<TextView
android:id="@+id/tvDevicesId"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:text="@string/devicesId"
android:textColor="@color/colorAccent"
android:textSize="20dp" />
```
##### 如果代码中动态控制了 ImageView 的 src、background 一定要在 XML 中替换成 SkinImageView.java
##### 代码中动态获取资源设置:图片、文字、颜色
```java
// 获取颜色
SkinResources.getInstance().getColor(resId);
// 获取图片
SkinResources.getInstance().getDrawable(resId);
// 获取文字
SkinResources.getInstance().getString(resId);
// 获取指定的资源id
SkinResources.getInstance().getIdentifier(resId);
// 获取指定的Raw资源 InputStream
SkinResources.getInstance().getRawInputStream(resId);
// 获取指定的Raw资源的byte[]
SkinResources.getInstance().getRawResourceBytes(resId);
```
#### 手动控制切换皮肤
```java
Skin skin = new Skin(
// 皮肤包 MD5 用于文件损坏校验的
"d5493244467d3970834e42dc1a6f07c9",
// 皮肤文件名称
"app-skin-debug.skin",
// 外网可访问的文件服务器地址
"https://carlife-static-1255510688.cos.ap-beijing.myqcloud.com/MoGoEagleEye/app-skin-debug.skin");
// 换肤
SkinManager.getInstance().selectSkin(this, skin);
// 还原
SkinManager.getInstance().loadSkin("");
```

View File

@@ -0,0 +1,37 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion rootProject.ext.android.compileSdkVersion
defaultConfig {
minSdkVersion rootProject.ext.android.minSdkVersion
targetSdkVersion rootProject.ext.android.targetSdkVersion
versionCode 1
versionName "${MOGO_REALTIME_VERSION}"
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation rootProject.ext.dependencies.androidxappcompat
}
apply from: new File(rootProject.rootDir, "gradle/upload.gradle").toString()

View File

@@ -0,0 +1,4 @@
GROUP=com.mogo.cloud
POM_ARTIFACT_ID=tts-mogo
VERSION_CODE=1
VERSION_NAME=1.0.1-SNAPSHOT

View 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

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.mogo.tts.offline">
</manifest>

View File

@@ -0,0 +1,34 @@
# Introduction
Model files are from
https://modelscope.cn/models/dengcunqin/matcha_tts_zh_en_20251010/summary
Note that you have to use
vocos-16khz-univ.onnx
You can download it from
https://modelscope.cn/models/dengcunqin/matcha_tts_zh_en_20251010/resolve/master/vocos-16khz-univ.onnx
or
https://github.com/k2-fsa/sherpa-onnx/releases/download/vocoder-models/vocos-16khz-univ.onnx
```
{'am': './model-steps-3.onnx', 'vocoder': './vocos-16khz-univ.onnx', 'tokens': './tokens.txt', 'lexicon': './lexicon.txt', 'text': '中英文合成测试. It supports both English 和中文合成', 'out_wav': 'generated.wav'}
{'use_eos_bos': '1', 'modelscope_url': 'https://modelscope.cn/models/dengcunqin/matcha_tts_zh_en_20251010', 'sample_rate': '16000', 'language': 'chinese English', 'model_type': 'matcha-tts', 'n_speakers': '1', 'model_author': 'dengcunqin', 'version': '1', 'pad_id': '0', 'voice': 'zh en-us', 'demo_url': 'https://www.tulingyun.com/tts.html', 'num_ode_steps': '3'}
NodeArg(name='x', type='tensor(int64)', shape=['N', 'L'])
NodeArg(name='x_length', type='tensor(int64)', shape=['N'])
NodeArg(name='noise_scale', type='tensor(float)', shape=[1])
NodeArg(name='length_scale', type='tensor(float)', shape=[1])
-----
NodeArg(name='mel', type='tensor(float)', shape=['N', 80, 'L'])
vocos {'modelscope_url': 'https://modelscope.cn/models/dengcunqin/matcha_tts_zh_en_20251010', 'use_eos_bos': '1', 'n_speakers': '1', 'sample_rate': '16000', 'pad_id': '0', 'language': 'chinese English', 'model_type': 'matcha-tts vocos', 'voice': 'zh en-us', 'version': '1', 'demo_url': 'https://www.tulingyun.com/tts.html', 'model_author': 'dengcunqin'}
----------vocos----------
NodeArg(name='mels', type='tensor(float)', shape=['batch_size', 80, 'time'])
-----
NodeArg(name='mag', type='tensor(float)', shape=['batch_size', 'Clipmag_dim_1', 'time'])
NodeArg(name='x', type='tensor(float)', shape=['batch_size', 'Cosx_dim_1', 'time'])
NodeArg(name='y', type='tensor(float)', shape=['batch_size', 'Cosx_dim_1', 'time'])
```

Some files were not shown because too many files have changed in this diff Show More