├── .gitignore ├── DemoMedia ├── ScreenRecord.mp4 └── ScreenShoot.png ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── utopiaxc │ │ └── utopiatts │ │ └── ExampleInstrumentedTest.java │ ├── debug │ ├── ic_launcher-playstore.png │ └── res │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ └── values │ │ └── ic_launcher_background.xml │ ├── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ │ └── com │ │ │ └── utopiaxc │ │ │ └── utopiatts │ │ │ ├── APP.java │ │ │ ├── MainActivity.java │ │ │ ├── engine │ │ │ ├── CheckVoiceData.java │ │ │ ├── GetSampleText.java │ │ │ └── UtopiaTtsService.java │ │ │ ├── enums │ │ │ ├── SettingsEnum.java │ │ │ └── ThemeModeEnum.java │ │ │ ├── fragments │ │ │ ├── FragmentAbout.java │ │ │ ├── FragmentTtsSettings.java │ │ │ └── FragmentTtsUsage.java │ │ │ ├── tts │ │ │ ├── MsTts.java │ │ │ ├── Tts.java │ │ │ ├── WsTts.java │ │ │ ├── enums │ │ │ │ ├── Actors.java │ │ │ │ ├── Driver.java │ │ │ │ ├── OutputFormat.java │ │ │ │ ├── Regions.java │ │ │ │ ├── Roles.java │ │ │ │ └── Styles.java │ │ │ └── utils │ │ │ │ ├── ByteArrayMediaDataSource.java │ │ │ │ ├── CommonTool.java │ │ │ │ ├── Constants.java │ │ │ │ ├── Ssml.java │ │ │ │ └── WebSocketState.java │ │ │ ├── usage │ │ │ ├── FragmentUsageDirect.java │ │ │ ├── FragmentUsageInnerApp.java │ │ │ └── FragmentUsageSystem.java │ │ │ ├── utils │ │ │ └── ThemeUtil.java │ │ │ └── welcome │ │ │ ├── FragmentSetAzureToken.kt │ │ │ ├── FragmentTtsSelect.kt │ │ │ └── IntroActivity.kt │ └── res │ │ ├── drawable-v24 │ │ └── logo.png │ │ ├── drawable │ │ ├── ic_baseline_code_24.xml │ │ ├── ic_baseline_help_outline_24.xml │ │ ├── ic_baseline_help_outline_24_night.xml │ │ ├── ic_baseline_settings_accessibility_24.xml │ │ └── ic_sharp_hearing_24.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── fragment_set_azure_token.xml │ │ ├── fragment_tts_select.xml │ │ └── fragment_tts_usage.xml │ │ ├── menu │ │ └── bottom_nav_menu.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── navigation │ │ └── nav_graph.xml │ │ ├── values-land │ │ └── dimens.xml │ │ ├── values-night │ │ └── themes.xml │ │ ├── values-w1240dp │ │ └── dimens.xml │ │ ├── values-w600dp │ │ └── dimens.xml │ │ ├── values-zh │ │ ├── arrays.xml │ │ └── strings.xml │ │ ├── values │ │ ├── arrays.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ ├── data_extraction_rules.xml │ │ ├── root_preferences.xml │ │ └── tts_engine.xml │ ├── release │ ├── ic_launcher-playstore.png │ └── res │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ └── values │ │ └── ic_launcher_background.xml │ └── test │ └── java │ └── com │ └── utopiaxc │ └── utopiatts │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | /.gradle/ 3 | /local.properties 4 | /.idea/ 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | local.properties 11 | /app/debug/ 12 | /app/release/ 13 | -------------------------------------------------------------------------------- /DemoMedia/ScreenRecord.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UtopiaXC/UtopiaTTS/b609bd98ba9244d11c3e8db13fc0380d3051766a/DemoMedia/ScreenRecord.mp4 -------------------------------------------------------------------------------- /DemoMedia/ScreenShoot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UtopiaXC/UtopiaTTS/b609bd98ba9244d11c3e8db13fc0380d3051766a/DemoMedia/ScreenShoot.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Utopia TTS (WIP) 2 | This is an Android TTS engine based on Microsoft TTS with personal token. 3 | 这是一个基于微软TTS的安卓文字转语音引擎,使用个人Token。 4 | 正在开发中 5 | 6 | ### 使用教程 7 | 暂无 8 | 9 | ### 鸣谢 10 | 大佬[ag2s20150909](https://github.com/ag2s20150909)的[TTS](https://github.com/ag2s20150909/TTS)项目 11 | 12 | ### 演示 13 | #### 应用截图 14 | ![App界面](DemoMedia/ScreenShoot.png) 15 | 16 | #### 演示视频 17 | 视频见[使用演示:medias/ScreenRecord.mp4](DemoMedia/ScreenRecord.mp4) 18 | 19 | ### 开发进程 20 | 21 | 20220929:微软仿佛有什么大病,才写了三天,五十万字的额度就耗尽了,这绝对不可能啊。算了,之后再说吧,躺了。至于官网Demo驱动的,还是去用ag2s写的TTS吧,日 22 | 23 | 20221202:尝试继承了一下ag2s大佬的ws方法,确实能用,等之后优化一下就可以发一版了 24 | 25 | 20230123:进行了一些简单的优化,发布一个[正式测试版](https://github.com/UtopiaXC/UtopiaTTS/releases/tag/0.0.1-beta03)。建议使用Demo WebSocket模式,同时选择东南亚可用区(这个可用区在我这测是网络问题最小的),需要的在[Release](https://github.com/UtopiaXC/UtopiaTTS/releases/tag/0.0.1-beta03)中自取。 -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | } 5 | 6 | android { 7 | namespace 'com.utopiaxc.utopiatts' 8 | compileSdk 33 9 | 10 | defaultConfig { 11 | applicationId "com.utopiaxc.utopiatts" 12 | minSdk 23 13 | targetSdk 33 14 | versionCode 3 15 | versionName "0.0.1 Beta03" 16 | 17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 18 | } 19 | 20 | buildTypes { 21 | release { 22 | minifyEnabled false 23 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 24 | } 25 | } 26 | compileOptions { 27 | sourceCompatibility JavaVersion.VERSION_1_8 28 | targetCompatibility JavaVersion.VERSION_1_8 29 | } 30 | buildFeatures { 31 | viewBinding true 32 | } 33 | } 34 | 35 | dependencies { 36 | 37 | implementation 'androidx.appcompat:appcompat:1.6.0' 38 | implementation 'com.google.android.material:material:1.7.0' 39 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4' 40 | implementation 'androidx.navigation:navigation-fragment:2.5.3' 41 | implementation 'androidx.navigation:navigation-ui:2.5.3' 42 | implementation 'androidx.preference:preference:1.2.0' 43 | testImplementation 'junit:junit:4.13.2' 44 | androidTestImplementation 'androidx.test.ext:junit:1.1.5' 45 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' 46 | 47 | //microsoft TTS 48 | implementation 'com.microsoft.cognitiveservices.speech:client-sdk:1.23.0' 49 | 50 | //welcome page 51 | implementation 'com.github.AppIntro:AppIntro:6.2.0' 52 | 53 | //about page 54 | implementation 'com.github.daniel-stoneuk:material-about-library:3.1.2' 55 | implementation "com.mikepenz:iconics-core:3.2.5" 56 | implementation "com.mikepenz:iconics-views:3.2.5" 57 | implementation 'com.mikepenz:google-material-typeface:3.0.1.3.original@aar' 58 | implementation 'com.mikepenz:material-design-iconic-typeface:2.2.0.5@aar' 59 | implementation 'com.mikepenz:fontawesome-typeface:5.3.1.1@aar' 60 | implementation 'com.mikepenz:octicons-typeface:3.2.0.5@aar' 61 | implementation 'com.mikepenz:meteocons-typeface:1.1.0.5@aar' 62 | implementation 'com.mikepenz:community-material-typeface:3.5.95.1@aar' 63 | implementation 'com.mikepenz:weather-icons-typeface:2.0.10.5@aar' 64 | implementation 'com.mikepenz:typeicons-typeface:2.0.7.5@aar' 65 | implementation 'com.mikepenz:entypo-typeface:1.0.0.5@aar' 66 | implementation 'com.mikepenz:devicon-typeface:2.0.0.5@aar' 67 | implementation 'com.mikepenz:foundation-icons-typeface:3.0.0.5@aar' 68 | implementation 'com.mikepenz:ionicons-typeface:2.0.1.5@aar' 69 | implementation 'com.mikepenz:pixeden-7-stroke-typeface:1.2.0.3@aar' 70 | 71 | //PersistentCookieJar 72 | implementation 'com.github.franmontiel:PersistentCookieJar:v1.0.1' 73 | 74 | //OKHTTP 75 | implementation "com.squareup.okhttp3:okhttp:5.0.0-alpha.10" 76 | implementation "com.squareup.okhttp3:okhttp-dnsoverhttps:5.0.0-alpha.10" 77 | implementation "com.squareup.okio:okio:3.2.0" 78 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/com/utopiaxc/utopiatts/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.utopiaxc.utopiatts; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.test.platform.app.InstrumentationRegistry; 6 | import androidx.test.ext.junit.runners.AndroidJUnit4; 7 | 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | 11 | import static org.junit.Assert.*; 12 | 13 | /** 14 | * Instrumented test, which will execute on an Android device. 15 | * 16 | * @see Testing documentation 17 | */ 18 | @RunWith(AndroidJUnit4.class) 19 | public class ExampleInstrumentedTest { 20 | @Test 21 | public void useAppContext() { 22 | // Context of the app under test. 23 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 24 | assertEquals("com.utopiaxc.utopiatts", appContext.getPackageName()); 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/debug/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UtopiaXC/UtopiaTTS/b609bd98ba9244d11c3e8db13fc0380d3051766a/app/src/debug/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UtopiaXC/UtopiaTTS/b609bd98ba9244d11c3e8db13fc0380d3051766a/app/src/debug/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UtopiaXC/UtopiaTTS/b609bd98ba9244d11c3e8db13fc0380d3051766a/app/src/debug/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UtopiaXC/UtopiaTTS/b609bd98ba9244d11c3e8db13fc0380d3051766a/app/src/debug/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UtopiaXC/UtopiaTTS/b609bd98ba9244d11c3e8db13fc0380d3051766a/app/src/debug/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UtopiaXC/UtopiaTTS/b609bd98ba9244d11c3e8db13fc0380d3051766a/app/src/debug/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UtopiaXC/UtopiaTTS/b609bd98ba9244d11c3e8db13fc0380d3051766a/app/src/debug/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UtopiaXC/UtopiaTTS/b609bd98ba9244d11c3e8db13fc0380d3051766a/app/src/debug/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UtopiaXC/UtopiaTTS/b609bd98ba9244d11c3e8db13fc0380d3051766a/app/src/debug/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UtopiaXC/UtopiaTTS/b609bd98ba9244d11c3e8db13fc0380d3051766a/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UtopiaXC/UtopiaTTS/b609bd98ba9244d11c3e8db13fc0380d3051766a/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UtopiaXC/UtopiaTTS/b609bd98ba9244d11c3e8db13fc0380d3051766a/app/src/debug/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UtopiaXC/UtopiaTTS/b609bd98ba9244d11c3e8db13fc0380d3051766a/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UtopiaXC/UtopiaTTS/b609bd98ba9244d11c3e8db13fc0380d3051766a/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UtopiaXC/UtopiaTTS/b609bd98ba9244d11c3e8db13fc0380d3051766a/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UtopiaXC/UtopiaTTS/b609bd98ba9244d11c3e8db13fc0380d3051766a/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/debug/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FAFAFA 4 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 24 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 36 | 37 | 38 | 41 | 42 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 56 | 57 | 58 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UtopiaXC/UtopiaTTS/b609bd98ba9244d11c3e8db13fc0380d3051766a/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/com/utopiaxc/utopiatts/APP.java: -------------------------------------------------------------------------------- 1 | package com.utopiaxc.utopiatts; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.app.Application; 5 | import android.content.Context; 6 | import android.content.SharedPreferences; 7 | import android.os.Handler; 8 | import android.os.Looper; 9 | import android.util.Log; 10 | import android.widget.Toast; 11 | 12 | import androidx.annotation.NonNull; 13 | import androidx.annotation.Nullable; 14 | 15 | import com.franmontiel.persistentcookiejar.PersistentCookieJar; 16 | import com.franmontiel.persistentcookiejar.cache.SetCookieCache; 17 | import com.franmontiel.persistentcookiejar.persistence.SharedPrefsCookiePersistor; 18 | 19 | import java.io.File; 20 | import java.net.InetAddress; 21 | import java.net.UnknownHostException; 22 | import java.util.Arrays; 23 | import java.util.Set; 24 | import java.util.concurrent.TimeUnit; 25 | 26 | import okhttp3.ConnectionSpec; 27 | import okhttp3.HttpUrl; 28 | import okhttp3.OkHttpClient; 29 | import okhttp3.dnsoverhttps.DnsOverHttps; 30 | 31 | @SuppressWarnings("unused") 32 | public class APP extends Application { 33 | /** 34 | * 全局的android.content.Context 35 | */ 36 | @SuppressLint("StaticFieldLeak") 37 | private static Context mContext; 38 | 39 | 40 | /** 41 | * 用于DoH的@see:okhttp3.OkHttpClient 42 | */ 43 | private static final OkHttpClient bootClient = new OkHttpClient.Builder() 44 | .connectionSpecs(Arrays.asList(ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS, ConnectionSpec.CLEARTEXT)) 45 | .fastFallback(true) 46 | .build(); 47 | private static volatile DnsOverHttps dns = null; 48 | 49 | private static DnsOverHttps getDns() { 50 | 51 | if (dns == null) { 52 | synchronized (APP.class) { 53 | if (dns == null) { 54 | OkHttpClient temp = APP.bootClient.newBuilder().cache(getCache("doh", 1024 * 1024 * 100)).build(); 55 | dns = new DnsOverHttps.Builder() 56 | .client(temp) 57 | .url(HttpUrl.get("https://dns.alidns.com/dns-query")) 58 | .bootstrapDnsHosts( 59 | getByName("223.5.5.5"), 60 | getByName("223.6.6.6"), 61 | getByName("2400:3200::1"), 62 | getByName("2400:3200:baba::1") 63 | ) 64 | 65 | .includeIPv6(true) 66 | .build(); 67 | } 68 | } 69 | } 70 | return dns; 71 | } 72 | //private static final DnsOverHttps dns = new DnsOverHttps.Builder().client( 73 | 74 | 75 | private static volatile OkHttpClient okHttpClient = null; 76 | 77 | public static OkHttpClient getOkHttpClient() { 78 | if (okHttpClient == null) { 79 | synchronized (APP.class) { 80 | if (okHttpClient == null) { 81 | 82 | okHttpClient = new OkHttpClient.Builder() 83 | .cookieJar(new PersistentCookieJar(new SetCookieCache(), 84 | new SharedPrefsCookiePersistor(getContext()))) 85 | .pingInterval(20, TimeUnit.SECONDS) // 设置 PING 帧发送间隔 86 | .fastFallback(true) 87 | .dns(s -> { 88 | // if("speech.platform.bing.com".equals(s)){ 89 | // s="cn.bing.com"; 90 | // } 91 | return getDns().lookup(s); 92 | }) 93 | .build(); 94 | } 95 | } 96 | } 97 | return okHttpClient; 98 | } 99 | 100 | 101 | public static void putString(String key, @Nullable String value) { 102 | SharedPreferences.Editor editor = getSharedPreferences().edit(); 103 | editor.putString(key, value); 104 | editor.apply(); 105 | 106 | } 107 | 108 | 109 | public static void putStringSet(String key, @Nullable Set values) { 110 | SharedPreferences.Editor editor = getSharedPreferences().edit(); 111 | editor.putStringSet(key, values); 112 | editor.apply(); 113 | 114 | } 115 | 116 | 117 | public static void putInt(String key, int value) { 118 | SharedPreferences.Editor editor = getSharedPreferences().edit(); 119 | editor.putInt(key, value); 120 | editor.apply(); 121 | 122 | } 123 | 124 | 125 | public static void putLong(String key, long value) { 126 | SharedPreferences.Editor editor = getSharedPreferences().edit(); 127 | editor.putLong(key, value); 128 | editor.apply(); 129 | } 130 | 131 | 132 | public static void putFloat(String key, float value) { 133 | SharedPreferences.Editor editor = getSharedPreferences().edit(); 134 | editor.putFloat(key, value); 135 | editor.apply(); 136 | 137 | } 138 | 139 | 140 | public static void putBoolean(String key, boolean value) { 141 | SharedPreferences.Editor editor = getSharedPreferences().edit(); 142 | editor.putBoolean(key, value); 143 | editor.apply(); 144 | 145 | } 146 | 147 | 148 | @Nullable 149 | public static String getString(String key, @Nullable String defValue) { 150 | return getSharedPreferences().getString(key, defValue); 151 | } 152 | 153 | 154 | @Nullable 155 | public static Set getStringSet(String key, @Nullable Set defValues) { 156 | return getSharedPreferences().getStringSet(key, defValues); 157 | } 158 | 159 | 160 | public static int getInt(String key, int defValue) { 161 | return getSharedPreferences().getInt(key, defValue); 162 | } 163 | 164 | 165 | public static long getLong(String key, long defValue) { 166 | return getSharedPreferences().getLong(key, defValue); 167 | } 168 | 169 | public static float getFloat(String key, float defValue) { 170 | return getSharedPreferences().getFloat(key, defValue); 171 | } 172 | 173 | 174 | public static boolean getBoolean(String key, boolean defValue) { 175 | return getSharedPreferences().getBoolean(key, defValue); 176 | } 177 | 178 | 179 | private static final class PreferencesHolder { 180 | public static final SharedPreferences preferences = getContext().getSharedPreferences("TTS", Context.MODE_PRIVATE); 181 | } 182 | 183 | public static SharedPreferences getSharedPreferences() { 184 | return PreferencesHolder.preferences; 185 | } 186 | 187 | public static @Nullable 188 | InetAddress getByName(@NonNull String ip) { 189 | 190 | try { 191 | return InetAddress.getByName(ip); 192 | } catch (UnknownHostException e) { 193 | return null; 194 | } 195 | } 196 | 197 | 198 | public static Context getContext() { 199 | if (mContext == null) { 200 | mContext = initAndGetAppCtxWithReflection(); 201 | } 202 | return mContext; 203 | } 204 | 205 | 206 | public static okhttp3.Cache getCache(String name, int maxSize) { 207 | File file = new File(getContext().getExternalCacheDir(), name); 208 | if (!file.exists()) { 209 | boolean mkdirs = file.mkdirs(); 210 | if (!mkdirs) { 211 | Log.e(APP.class.getSimpleName(), "创建文件夹失败"); 212 | } 213 | } 214 | return new okhttp3.Cache(file, maxSize); 215 | } 216 | 217 | 218 | /** 219 | * 反射获取Context 220 | */ 221 | @SuppressLint({"DiscouragedPrivateApi", "PrivateApi"}) 222 | private static Context initAndGetAppCtxWithReflection() { 223 | // Fallback, should only run once per non default process. 224 | try { 225 | return (Context) Class.forName("android.app.ActivityThread") 226 | .getDeclaredMethod("currentApplication") 227 | .invoke(null); 228 | } catch (Exception e) { 229 | return null; 230 | } 231 | 232 | } 233 | 234 | 235 | @Override 236 | public void onCreate() { 237 | super.onCreate(); 238 | //Security.insertProviderAt(Conscrypt.newProvider(), 1); 239 | mContext = this.getApplicationContext(); 240 | } 241 | 242 | public static void showToast(String msg) { 243 | if (isMainThread()) { 244 | Toast.makeText(getContext(), msg, Toast.LENGTH_LONG).show(); 245 | } else { 246 | new Handler(Looper.getMainLooper()).post(() -> Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show()); 247 | } 248 | } 249 | 250 | public static boolean isMainThread() { 251 | return Looper.getMainLooper().getThread().getId() == Thread.currentThread().getId(); 252 | } 253 | 254 | @Override 255 | protected void attachBaseContext(Context base) { 256 | super.attachBaseContext(base); 257 | //Security.insertProviderAt(Conscrypt.newProvider(), 1); 258 | mContext = getApplicationContext(); 259 | } 260 | } -------------------------------------------------------------------------------- /app/src/main/java/com/utopiaxc/utopiatts/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.utopiaxc.utopiatts; 2 | 3 | import android.content.Intent; 4 | import android.content.SharedPreferences; 5 | import android.os.Bundle; 6 | 7 | import androidx.appcompat.app.AppCompatActivity; 8 | import androidx.navigation.NavController; 9 | import androidx.navigation.Navigation; 10 | import androidx.navigation.ui.AppBarConfiguration; 11 | import androidx.navigation.ui.NavigationUI; 12 | import androidx.preference.PreferenceManager; 13 | 14 | import com.utopiaxc.utopiatts.databinding.ActivityMainBinding; 15 | import com.utopiaxc.utopiatts.enums.SettingsEnum; 16 | import com.utopiaxc.utopiatts.tts.MsTts; 17 | import com.utopiaxc.utopiatts.tts.WsTts; 18 | import com.utopiaxc.utopiatts.tts.enums.Driver; 19 | import com.utopiaxc.utopiatts.utils.ThemeUtil; 20 | import com.utopiaxc.utopiatts.welcome.IntroActivity; 21 | 22 | public class MainActivity extends AppCompatActivity { 23 | @Override 24 | protected void onCreate(Bundle savedInstanceState) { 25 | super.onCreate(savedInstanceState); 26 | com.utopiaxc.utopiatts.databinding.ActivityMainBinding binding = 27 | ActivityMainBinding.inflate(getLayoutInflater()); 28 | setContentView(binding.getRoot()); 29 | //Night mode 30 | SharedPreferences sharedPreferences = 31 | PreferenceManager.getDefaultSharedPreferences(this); 32 | ThemeUtil.setThemeMode(sharedPreferences.getString(SettingsEnum.THEME.getKey(), 33 | (String) SettingsEnum.THEME.getDefaultValue())); 34 | //First install check 35 | if (sharedPreferences.getBoolean(SettingsEnum.FIRST_BOOT.getKey(), 36 | (Boolean) SettingsEnum.FIRST_BOOT.getDefaultValue())) { 37 | //Welcome 38 | Intent intent = new Intent(this, IntroActivity.class); 39 | startActivity(intent); 40 | finish(); 41 | return; 42 | } 43 | 44 | //Init tts engine 45 | if (sharedPreferences.getString(SettingsEnum.TTS_DRIVER.getKey(), Driver.AZURE_SDK.getId()) 46 | .equals(Driver.AZURE_SDK.getId())){ 47 | MsTts.getInstance(getApplicationContext()); 48 | }else{ 49 | WsTts.getInstance(getApplicationContext()); 50 | } 51 | AppBarConfiguration appBarConfiguration = new AppBarConfiguration.Builder( 52 | R.id.fragment_tts_settings, R.id.fragment_tts_usage, R.id.fragment_about) 53 | .build(); 54 | NavController navController = 55 | Navigation.findNavController(this, R.id.nav_host_fragment_activity_main); 56 | NavigationUI 57 | .setupActionBarWithNavController(this, navController, appBarConfiguration); 58 | NavigationUI.setupWithNavController(binding.navView, navController); 59 | } 60 | } -------------------------------------------------------------------------------- /app/src/main/java/com/utopiaxc/utopiatts/engine/CheckVoiceData.java: -------------------------------------------------------------------------------- 1 | package com.utopiaxc.utopiatts.engine; 2 | 3 | import android.app.Activity; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.speech.tts.TextToSpeech; 7 | import android.util.Log; 8 | 9 | import java.util.ArrayList; 10 | import java.util.Arrays; 11 | 12 | public class CheckVoiceData extends Activity { 13 | private static final String TAG = CheckVoiceData.class.getSimpleName(); 14 | 15 | @Override 16 | protected void onCreate(Bundle savedInstanceState) { 17 | super.onCreate(savedInstanceState); 18 | int result = TextToSpeech.Engine.CHECK_VOICE_DATA_PASS; 19 | 20 | ArrayList unavailable = new ArrayList<>(); 21 | 22 | ArrayList available = new ArrayList<>( 23 | Arrays.asList("zho-CHN", "zho-HKG", "zho-TWN")); 24 | 25 | 26 | Intent returnData = new Intent(); 27 | Log.i(TAG, available.toString()); 28 | 29 | returnData.putStringArrayListExtra(TextToSpeech.Engine.EXTRA_AVAILABLE_VOICES, available); 30 | returnData.putStringArrayListExtra( 31 | TextToSpeech.Engine.EXTRA_UNAVAILABLE_VOICES, unavailable); 32 | setResult(result, returnData); 33 | finish(); 34 | } 35 | } -------------------------------------------------------------------------------- /app/src/main/java/com/utopiaxc/utopiatts/engine/GetSampleText.java: -------------------------------------------------------------------------------- 1 | package com.utopiaxc.utopiatts.engine; 2 | 3 | import android.app.Activity; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.speech.tts.TextToSpeech; 7 | import android.util.Log; 8 | 9 | import com.utopiaxc.utopiatts.R; 10 | 11 | import java.util.Locale; 12 | 13 | public class GetSampleText extends Activity { 14 | private static final String TAG = "GetSampleText"; 15 | 16 | @Override 17 | protected void onCreate(Bundle savedInstanceState) { 18 | Log.i(TAG, "onCreate"); 19 | super.onCreate(savedInstanceState); 20 | int result = TextToSpeech.LANG_AVAILABLE; 21 | Intent returnData = new Intent(); 22 | 23 | Intent i = getIntent(); 24 | String language = i.getExtras().getString("language"); 25 | String country = i.getExtras().getString("country"); 26 | String variant = i.getExtras().getString("variant"); 27 | Log.i(TAG, language + "_" + country + "_" + variant); 28 | Locale locale = new Locale(language, country); 29 | returnData.putExtra("sampleText", getText(R.string.tts_demo_text)); 30 | setResult(result, returnData); 31 | finish(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/utopiaxc/utopiatts/engine/UtopiaTtsService.java: -------------------------------------------------------------------------------- 1 | package com.utopiaxc.utopiatts.engine; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.app.Notification; 5 | import android.app.NotificationChannel; 6 | import android.app.NotificationManager; 7 | import android.app.PendingIntent; 8 | import android.content.Context; 9 | import android.content.Intent; 10 | import android.content.SharedPreferences; 11 | import android.os.Build; 12 | import android.speech.tts.SynthesisCallback; 13 | import android.speech.tts.SynthesisRequest; 14 | import android.speech.tts.TextToSpeech; 15 | import android.speech.tts.TextToSpeechService; 16 | import android.util.Log; 17 | 18 | import androidx.preference.PreferenceManager; 19 | 20 | import com.utopiaxc.utopiatts.enums.SettingsEnum; 21 | import com.utopiaxc.utopiatts.tts.MsTts; 22 | import com.utopiaxc.utopiatts.tts.Tts; 23 | import com.utopiaxc.utopiatts.tts.WsTts; 24 | import com.utopiaxc.utopiatts.tts.enums.Driver; 25 | import com.utopiaxc.utopiatts.tts.enums.OutputFormat; 26 | 27 | import java.util.ArrayList; 28 | import java.util.Arrays; 29 | import java.util.List; 30 | import java.util.Locale; 31 | 32 | public class UtopiaTtsService extends TextToSpeechService { 33 | private static final String TAG = "UtopiaTtsService"; 34 | private volatile String[] mCurrentLanguage = null; 35 | NotificationManager notificationManager; 36 | Notification.Builder notificationBuilder; 37 | final String notificationChannelId = UtopiaTtsService.class.getName(); 38 | final String notificationName = "Utopia TTS Service Notification"; 39 | private static final int NOTIFICATION_ID = 1; 40 | private static final String ACTION_STOP_SERVICE = "action_stop_service"; 41 | private Tts mTts; 42 | private Thread mSynthesizeTextThread; 43 | 44 | @Override 45 | public void onCreate() { 46 | Log.i(TAG, "onCreate"); 47 | super.onCreate(); 48 | if (PreferenceManager.getDefaultSharedPreferences(this) 49 | .getString(SettingsEnum.TTS_DRIVER.getKey(), Driver.AZURE_SDK.getId()) 50 | .equals(Driver.AZURE_SDK.getId())) { 51 | mTts = MsTts.getInstance(getApplicationContext()); 52 | } else { 53 | mTts = WsTts.getInstance(getApplicationContext()); 54 | } 55 | startForegroundService(); 56 | } 57 | 58 | @Override 59 | public void onDestroy() { 60 | Log.i(TAG, "onDestroy"); 61 | mTts.stopSpeak(); 62 | if (mSynthesizeTextThread != null) { 63 | mSynthesizeTextThread.interrupt(); 64 | } 65 | super.onDestroy(); 66 | } 67 | 68 | @Override 69 | protected int onIsLanguageAvailable(String lang, String country, String variant) { 70 | if ((Locale.SIMPLIFIED_CHINESE.getISO3Language().equals(lang)) || 71 | (Locale.US.getISO3Language().equals(lang))) { 72 | if ((Locale.SIMPLIFIED_CHINESE.getISO3Country().equals(country)) || 73 | (Locale.US.getISO3Country().equals(country))) { 74 | return TextToSpeech.LANG_COUNTRY_AVAILABLE; 75 | } 76 | return TextToSpeech.LANG_AVAILABLE; 77 | } 78 | return TextToSpeech.LANG_NOT_SUPPORTED; 79 | } 80 | 81 | @Override 82 | protected String[] onGetLanguage() { 83 | return mCurrentLanguage; 84 | } 85 | 86 | @Override 87 | protected int onLoadLanguage(String lang, String country, String variant) { 88 | lang = lang == null ? "" : lang; 89 | country = country == null ? "" : country; 90 | variant = variant == null ? "" : variant; 91 | int result = onIsLanguageAvailable(lang, country, variant); 92 | if (result == TextToSpeech.LANG_COUNTRY_AVAILABLE || 93 | TextToSpeech.LANG_AVAILABLE == result || 94 | result == TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE) { 95 | mCurrentLanguage = new String[]{lang, country, variant}; 96 | } 97 | return result; 98 | } 99 | 100 | @Override 101 | protected void onStop() { 102 | Log.i(TAG, "onStop"); 103 | mTts.stopSpeak(); 104 | if (mSynthesizeTextThread != null) { 105 | mSynthesizeTextThread.interrupt(); 106 | } 107 | } 108 | 109 | @SuppressLint("WrongConstant") 110 | @Override 111 | protected void onSynthesizeText(SynthesisRequest synthesisRequest, 112 | SynthesisCallback synthesisCallback) { 113 | Log.i(TAG, "onSynthesizeText"); 114 | int load = onLoadLanguage(synthesisRequest.getLanguage(), synthesisRequest.getCountry(), 115 | synthesisRequest.getVariant()); 116 | if (load == TextToSpeech.LANG_NOT_SUPPORTED) { 117 | synthesisCallback.error(); 118 | Log.i(TAG, "LANG_NOT_SUPPORTED"); 119 | return; 120 | } 121 | SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); 122 | OutputFormat outputFormat = OutputFormat.getOutputFormat( 123 | preferences.getString(SettingsEnum.OUTPUT_FORMAT.getKey(), 124 | (String) SettingsEnum.OUTPUT_FORMAT.getDefaultValue())); 125 | synthesisCallback.start(outputFormat.getSoundFrequency(), 126 | outputFormat.getAudioFormat(), 1); 127 | mSynthesizeTextThread = new Thread(new SynthesizeText(synthesisRequest, synthesisCallback)); 128 | mSynthesizeTextThread.start(); 129 | try { 130 | mSynthesizeTextThread.join(); 131 | } catch (InterruptedException e) { 132 | e.printStackTrace(); 133 | } 134 | } 135 | 136 | class SynthesizeText implements Runnable { 137 | SynthesisRequest synthesisRequest; 138 | SynthesisCallback synthesisCallback; 139 | 140 | public SynthesizeText(SynthesisRequest synthesisRequest, 141 | SynthesisCallback synthesisCallback) { 142 | this.synthesisRequest = synthesisRequest; 143 | this.synthesisCallback = synthesisCallback; 144 | } 145 | 146 | @Override 147 | public void run() { 148 | String textToSpeech = synthesisRequest.getCharSequenceText().toString(); 149 | List text = new ArrayList<>(); 150 | StringBuilder textBuilder = new StringBuilder(); 151 | for (int i = 0; i < textToSpeech.length(); i++) { 152 | char c = textToSpeech.charAt(i); 153 | if (".。!!??".contains(String.valueOf(c))) { 154 | textBuilder.append(c); 155 | if (textBuilder.length() >= 50) { 156 | text.add(textBuilder.toString()); 157 | textBuilder.setLength(0); 158 | } 159 | } else { 160 | textBuilder.append(c); 161 | } 162 | } 163 | text.add(textBuilder.toString()); 164 | for (String s : text) { 165 | Log.i(TAG, s); 166 | if (!mTts.doSpeak(s, 167 | synthesisRequest.getPitch(), synthesisRequest.getSpeechRate(), 168 | synthesisCallback)) { 169 | synthesisCallback.error(); 170 | return; 171 | } 172 | } 173 | synthesisCallback.done(); 174 | } 175 | } 176 | 177 | private void startForegroundService() { 178 | notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); 179 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 180 | NotificationChannel notificationChannel = new NotificationChannel(notificationChannelId, 181 | notificationName, NotificationManager.IMPORTANCE_LOW); 182 | notificationManager.createNotificationChannel(notificationChannel); 183 | 184 | } 185 | startForeground(NOTIFICATION_ID, getNotification()); 186 | } 187 | 188 | private Notification getNotification() { 189 | Intent stopSelf = new Intent(this, UtopiaTtsService.class); 190 | stopSelf.setAction(ACTION_STOP_SERVICE); 191 | PendingIntent pStopSelf = PendingIntent.getService(this, 0, stopSelf, 192 | PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); 193 | notificationBuilder = new Notification.Builder(this) 194 | .setOnlyAlertOnce(true) 195 | .setVibrate(null) 196 | .setSound(null) 197 | .setLights(0, 0, 0) 198 | .setContentTitle("Utopia TTS Service") 199 | .setContentText("UtopiaTTS Service is running..."); 200 | 201 | Notification.Action action; 202 | action = new Notification.Action.Builder(null, "stop", pStopSelf).build(); 203 | notificationBuilder.addAction(action); 204 | 205 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 206 | notificationBuilder.setChannelId(notificationChannelId); 207 | } 208 | return notificationBuilder.build(); 209 | } 210 | } -------------------------------------------------------------------------------- /app/src/main/java/com/utopiaxc/utopiatts/enums/SettingsEnum.java: -------------------------------------------------------------------------------- 1 | package com.utopiaxc.utopiatts.enums; 2 | 3 | import com.utopiaxc.utopiatts.tts.enums.Driver; 4 | import com.utopiaxc.utopiatts.tts.enums.OutputFormat; 5 | 6 | public enum SettingsEnum { 7 | 8 | THEME("THEME", "theme", "auto"), 9 | FIRST_BOOT("FIRST_BOOT", "first_boot", true), 10 | AZURE_REGION("AZURE_REGION", "azure_region", ""), 11 | AZURE_TOKEN("AZURE_TOKEN", "azure_token", "NULL"), 12 | OUTPUT_FORMAT("OUTPUT_FORMAT", "output_format", 13 | OutputFormat.OGG_48K_HZ_16_BIT_MONO_OPUS.getName()), 14 | ACTOR("ACTOR", "actor", ""), 15 | ROLE("ROLE", "role", ""), 16 | STYLE("STYLE", "style", ""), 17 | STYLE_DEGREE("STYLE_DEGREE", "style_degree", 0), 18 | TTS_DRIVER("TTS_DRIVER", "tts_driver", Driver.AZURE_SDK.getId()); 19 | private final String mName; 20 | private final String mKey; 21 | private final Object mDefaultValue; 22 | 23 | SettingsEnum(String name, String key, Object defaultValue) { 24 | mName = name; 25 | mKey = key; 26 | mDefaultValue = defaultValue; 27 | } 28 | 29 | public String getName() { 30 | return mName; 31 | } 32 | 33 | public String getKey() { 34 | return mKey; 35 | } 36 | 37 | public Object getDefaultValue() { 38 | return mDefaultValue; 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/utopiaxc/utopiatts/enums/ThemeModeEnum.java: -------------------------------------------------------------------------------- 1 | package com.utopiaxc.utopiatts.enums; 2 | 3 | public enum ThemeModeEnum { 4 | AUTO_MODE("auto", "Auto Mode"), 5 | DAY_MODE("day", "Day Mode"), 6 | NIGHT_MODE("night", "Night Mode"); 7 | 8 | private final String mode; 9 | private final String description; 10 | ThemeModeEnum(String mode, String description) { 11 | this.mode = mode; 12 | this.description = description; 13 | } 14 | 15 | public String getMode() { 16 | return mode; 17 | } 18 | public String getDescription() { 19 | return description; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/utopiaxc/utopiatts/fragments/FragmentAbout.java: -------------------------------------------------------------------------------- 1 | package com.utopiaxc.utopiatts.fragments; 2 | 3 | import android.app.AlertDialog; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.net.Uri; 7 | 8 | import androidx.preference.PreferenceManager; 9 | 10 | import com.danielstone.materialaboutlibrary.ConvenienceBuilder; 11 | import com.danielstone.materialaboutlibrary.MaterialAboutFragment; 12 | import com.danielstone.materialaboutlibrary.items.MaterialAboutActionItem; 13 | import com.danielstone.materialaboutlibrary.items.MaterialAboutTitleItem; 14 | import com.danielstone.materialaboutlibrary.model.MaterialAboutCard; 15 | import com.danielstone.materialaboutlibrary.model.MaterialAboutList; 16 | import com.mikepenz.community_material_typeface_library.CommunityMaterial; 17 | import com.mikepenz.iconics.IconicsDrawable; 18 | import com.utopiaxc.utopiatts.MainActivity; 19 | import com.utopiaxc.utopiatts.R; 20 | 21 | public class FragmentAbout extends MaterialAboutFragment { 22 | 23 | @Override 24 | protected MaterialAboutList getMaterialAboutList(Context activityContext) { 25 | //app cards 26 | MaterialAboutCard.Builder appCardBuilder = new MaterialAboutCard.Builder(); 27 | 28 | //copyright card 29 | appCardBuilder.addItem(new MaterialAboutTitleItem.Builder() 30 | .text(R.string.app_name) 31 | .desc(R.string.rights) 32 | .icon(R.mipmap.ic_launcher) 33 | .build()); 34 | 35 | //version card 36 | appCardBuilder.addItem(ConvenienceBuilder.createVersionActionItem(activityContext, 37 | new IconicsDrawable(activityContext) 38 | .icon(CommunityMaterial.Icon.cmd_code_array) 39 | .sizeDp(18), 40 | requireActivity().getString(R.string.version), 41 | true)); 42 | 43 | //change log card 44 | appCardBuilder.addItem(new MaterialAboutActionItem.Builder() 45 | .text(R.string.change_log) 46 | .icon(new IconicsDrawable(activityContext) 47 | .icon(CommunityMaterial.Icon.cmd_content_paste) 48 | .sizeDp(18)) 49 | .setOnClickAction(ConvenienceBuilder.createWebViewDialogOnClickAction( 50 | activityContext, 51 | requireActivity().getString(R.string.change_log_title), 52 | getString(R.string.github_release_link), true, false)) 53 | .build()); 54 | 55 | //license card 56 | appCardBuilder.addItem(new MaterialAboutActionItem.Builder() 57 | .text(R.string.licenses) 58 | .icon(new IconicsDrawable(activityContext) 59 | .icon(CommunityMaterial.Icon.cmd_code_tags) 60 | .sizeDp(18)) 61 | .setOnClickAction(() -> { 62 | //Intent intent = new Intent(activityContext, LicencesActivity.class); 63 | //startActivity(intent); 64 | }) 65 | .build()); 66 | 67 | //author cards 68 | MaterialAboutCard.Builder authorCardBuilder = new MaterialAboutCard.Builder(); 69 | authorCardBuilder.title(R.string.author); 70 | 71 | //author card 72 | authorCardBuilder.addItem(new MaterialAboutActionItem.Builder() 73 | .text(R.string.author_name) 74 | .icon(new IconicsDrawable(activityContext) 75 | .icon(CommunityMaterial.Icon.cmd_account) 76 | .sizeDp(18)) 77 | .build()); 78 | 79 | //github card 80 | authorCardBuilder.addItem(new MaterialAboutActionItem.Builder() 81 | .text(R.string.follow_on_github) 82 | .icon(new IconicsDrawable(activityContext) 83 | .icon(CommunityMaterial.Icon.cmd_github_circle) 84 | .sizeDp(18)) 85 | .setOnClickAction(ConvenienceBuilder.createWebsiteOnClickAction( 86 | activityContext, Uri.parse(getString(R.string.github_link)))) 87 | .build()); 88 | 89 | //email feedback card 90 | authorCardBuilder.addItem(ConvenienceBuilder.createEmailItem(activityContext, 91 | new IconicsDrawable(activityContext) 92 | .icon(CommunityMaterial.Icon.cmd_email) 93 | .sizeDp(18), 94 | requireActivity().getString(R.string.feedback_by_email), 95 | true, 96 | getString(R.string.user_email), 97 | requireActivity().getString(R.string.feedback_subject))); 98 | 99 | //settings card 100 | MaterialAboutCard.Builder settingsCardBuilder = new MaterialAboutCard.Builder(); 101 | settingsCardBuilder.title(R.string.settings); 102 | 103 | //clear profiles 104 | settingsCardBuilder.addItem(new MaterialAboutActionItem.Builder() 105 | .text(R.string.clear_all_profiles) 106 | .icon(new IconicsDrawable(activityContext) 107 | .icon(CommunityMaterial.Icon.cmd_delete) 108 | .sizeDp(18)) 109 | .setOnClickAction(() -> new AlertDialog.Builder(getContext()) 110 | .setTitle(R.string.warning) 111 | .setMessage(R.string.clear_all_profiles_confirm) 112 | .setPositiveButton(R.string.confirm, (dialog, which) -> { 113 | PreferenceManager.getDefaultSharedPreferences(requireActivity()) 114 | .edit().clear().apply(); 115 | Intent intent = new Intent(requireContext(), MainActivity.class); 116 | startActivity(intent); 117 | requireActivity().finish(); 118 | }) 119 | .setNegativeButton(R.string.cancel, null) 120 | .create() 121 | .show()) 122 | .build()); 123 | return new MaterialAboutList(appCardBuilder.build(), 124 | authorCardBuilder.build(), 125 | settingsCardBuilder.build()); 126 | } 127 | 128 | @Override 129 | public void onDestroyView() { 130 | super.onDestroyView(); 131 | } 132 | } -------------------------------------------------------------------------------- /app/src/main/java/com/utopiaxc/utopiatts/fragments/FragmentTtsSettings.java: -------------------------------------------------------------------------------- 1 | package com.utopiaxc.utopiatts.fragments; 2 | 3 | import android.app.AlertDialog; 4 | import android.content.SharedPreferences; 5 | import android.os.Bundle; 6 | import android.text.InputType; 7 | import android.util.Log; 8 | 9 | import androidx.preference.EditTextPreference; 10 | import androidx.preference.ListPreference; 11 | import androidx.preference.PreferenceFragmentCompat; 12 | import androidx.preference.PreferenceManager; 13 | 14 | 15 | import com.utopiaxc.utopiatts.R; 16 | import com.utopiaxc.utopiatts.enums.SettingsEnum; 17 | import com.utopiaxc.utopiatts.tts.MsTts; 18 | import com.utopiaxc.utopiatts.tts.Tts; 19 | import com.utopiaxc.utopiatts.tts.WsTts; 20 | import com.utopiaxc.utopiatts.tts.enums.Actors; 21 | import com.utopiaxc.utopiatts.tts.enums.Driver; 22 | import com.utopiaxc.utopiatts.tts.enums.Regions; 23 | 24 | import java.util.Objects; 25 | 26 | public class FragmentTtsSettings extends PreferenceFragmentCompat { 27 | 28 | @Override 29 | public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { 30 | Tts tts; 31 | SharedPreferences sharedPreferences = 32 | PreferenceManager.getDefaultSharedPreferences(requireActivity()); 33 | if (sharedPreferences.getString(SettingsEnum.TTS_DRIVER.getKey(), Driver.AZURE_SDK.getId()) 34 | .equals(Driver.AZURE_SDK.getId())) { 35 | tts = MsTts.getInstance(requireActivity().getApplicationContext()); 36 | } else { 37 | tts = WsTts.getInstance(requireActivity().getApplicationContext()); 38 | } 39 | setPreferencesFromResource(R.xml.root_preferences, rootKey); 40 | EditTextPreference azureToken = findPreference(SettingsEnum.AZURE_TOKEN.getKey()); 41 | Objects.requireNonNull(azureToken).setOnBindEditTextListener(editText -> editText 42 | .setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD)); 43 | azureToken.setSummary(null); 44 | azureToken.setOnPreferenceChangeListener((preference, newValue) -> { 45 | boolean ok = MsTts.testAzureConfig((String) newValue, 46 | Regions.getRegion(PreferenceManager 47 | .getDefaultSharedPreferences(requireActivity()) 48 | .getString(SettingsEnum.AZURE_REGION.getKey(), 49 | (String) SettingsEnum.AZURE_REGION.getDefaultValue())) 50 | .getId()); 51 | if (ok) { 52 | new AlertDialog.Builder(requireActivity()).setTitle(R.string.success) 53 | .setMessage(R.string.success_azure_token) 54 | .setPositiveButton(R.string.confirm, null) 55 | .create() 56 | .show(); 57 | } else { 58 | new AlertDialog.Builder(requireActivity()).setTitle(R.string.error) 59 | .setMessage(R.string.error_azure_token) 60 | .setPositiveButton(R.string.confirm, null) 61 | .create() 62 | .show(); 63 | } 64 | tts.initTts(); 65 | return ok; 66 | }); 67 | 68 | ListPreference listActors = findPreference(SettingsEnum.ACTOR.getKey()); 69 | assert listActors != null; 70 | listActors.setOnPreferenceChangeListener((preference, newValue) -> { 71 | if (Actors.getActor((String) newValue).isPre()) { 72 | if (Regions.getRegion(sharedPreferences.getString(SettingsEnum.AZURE_REGION 73 | .getKey(), 74 | String.valueOf(SettingsEnum.AZURE_REGION.getDefaultValue()))) 75 | .isNotSupportPre()) { 76 | new AlertDialog.Builder(requireActivity()).setTitle(R.string.error) 77 | .setMessage(R.string.region_not_support_pre) 78 | .setPositiveButton(R.string.confirm, null) 79 | .create() 80 | .show(); 81 | return false; 82 | } 83 | } 84 | tts.initTts(); 85 | return true; 86 | }); 87 | 88 | ListPreference listRegion = findPreference(SettingsEnum.AZURE_REGION.getKey()); 89 | assert listRegion != null; 90 | listRegion.setOnPreferenceChangeListener((preference, newValue) -> { 91 | if (Actors.getActor(sharedPreferences.getString(SettingsEnum.ACTOR.getKey(), 92 | (String) SettingsEnum.ACTOR.getDefaultValue())).isPre()) { 93 | if (Regions.getRegion((String) newValue).isNotSupportPre()) { 94 | new AlertDialog.Builder(requireActivity()).setTitle(R.string.error) 95 | .setMessage(R.string.actor_is_pre) 96 | .setPositiveButton(R.string.confirm, null) 97 | .create() 98 | .show(); 99 | } 100 | return false; 101 | } 102 | return true; 103 | }); 104 | 105 | ListPreference listFormat = findPreference(SettingsEnum.OUTPUT_FORMAT.getKey()); 106 | assert listFormat != null; 107 | listFormat.setOnPreferenceChangeListener((preference, newValue) -> { 108 | new AlertDialog.Builder(requireActivity()).setTitle(R.string.warning) 109 | .setMessage(R.string.warning_of_output_format) 110 | .setPositiveButton(R.string.confirm, null) 111 | .create() 112 | .show(); 113 | return true; 114 | }); 115 | 116 | EditTextPreference editTextPreferenceToken = findPreference( 117 | SettingsEnum.AZURE_TOKEN.getKey()); 118 | assert editTextPreferenceToken != null; 119 | editTextPreferenceToken.setEnabled(Driver.AZURE_SDK.getId().equals( 120 | sharedPreferences.getString(SettingsEnum.TTS_DRIVER.getKey(), 121 | String.valueOf(SettingsEnum.TTS_DRIVER.getDefaultValue())))); 122 | 123 | ListPreference listDriver = findPreference(SettingsEnum.TTS_DRIVER.getKey()); 124 | assert listDriver != null; 125 | listDriver.setOnPreferenceChangeListener((preference, newValue) -> { 126 | editTextPreferenceToken.setEnabled(Driver.AZURE_SDK.getId().equals(newValue)); 127 | return true; 128 | }); 129 | } 130 | } -------------------------------------------------------------------------------- /app/src/main/java/com/utopiaxc/utopiatts/fragments/FragmentTtsUsage.java: -------------------------------------------------------------------------------- 1 | package com.utopiaxc.utopiatts.fragments; 2 | 3 | import android.os.Bundle; 4 | 5 | import androidx.fragment.app.Fragment; 6 | 7 | import android.view.LayoutInflater; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | import com.utopiaxc.utopiatts.databinding.FragmentTtsUsageBinding; 11 | 12 | import com.utopiaxc.utopiatts.R; 13 | 14 | public class FragmentTtsUsage extends Fragment { 15 | public FragmentTtsUsage() { 16 | 17 | } 18 | 19 | public static FragmentTtsUsage newInstance() { 20 | return new FragmentTtsUsage(); 21 | } 22 | 23 | @Override 24 | public void onCreate(Bundle savedInstanceState) { 25 | super.onCreate(savedInstanceState); 26 | } 27 | 28 | @Override 29 | public View onCreateView(LayoutInflater inflater, ViewGroup container, 30 | Bundle savedInstanceState) { 31 | return inflater.inflate(R.layout.fragment_tts_usage, container, false); 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/com/utopiaxc/utopiatts/tts/MsTts.java: -------------------------------------------------------------------------------- 1 | package com.utopiaxc.utopiatts.tts; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | import android.speech.tts.SynthesisCallback; 6 | import android.util.Log; 7 | 8 | import androidx.preference.PreferenceManager; 9 | 10 | import com.microsoft.cognitiveservices.speech.CancellationReason; 11 | import com.microsoft.cognitiveservices.speech.ResultReason; 12 | import com.microsoft.cognitiveservices.speech.SpeechConfig; 13 | import com.microsoft.cognitiveservices.speech.SpeechSynthesisCancellationDetails; 14 | import com.microsoft.cognitiveservices.speech.SpeechSynthesisResult; 15 | import com.microsoft.cognitiveservices.speech.SpeechSynthesizer; 16 | import com.microsoft.cognitiveservices.speech.audio.AudioConfig; 17 | import com.utopiaxc.utopiatts.enums.SettingsEnum; 18 | import com.utopiaxc.utopiatts.tts.enums.Actors; 19 | import com.utopiaxc.utopiatts.tts.enums.OutputFormat; 20 | import com.utopiaxc.utopiatts.tts.enums.Regions; 21 | import com.utopiaxc.utopiatts.tts.enums.Roles; 22 | import com.utopiaxc.utopiatts.tts.enums.Styles; 23 | import com.utopiaxc.utopiatts.tts.utils.Ssml; 24 | 25 | import java.util.concurrent.ExecutionException; 26 | 27 | public class MsTts implements Tts{ 28 | private static final String TAG = "MsTts"; 29 | private static volatile Tts mInstance; 30 | private SpeechSynthesizer mSpeechSynthesizer; 31 | SharedPreferences mSharedPreferences; 32 | 33 | public MsTts(Context context) { 34 | mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); 35 | initTts(); 36 | } 37 | 38 | public static Tts getInstance(Context context) { 39 | if (mInstance == null) { 40 | synchronized (MsTts.class) { 41 | if (mInstance == null) { 42 | mInstance = new MsTts(context.getApplicationContext()); 43 | } 44 | } 45 | } 46 | return mInstance; 47 | } 48 | 49 | @Override 50 | public boolean doSpeak(String text, int pitch, int rate, SynthesisCallback synthesisCallback) { 51 | Actors actor = Actors.getActor( 52 | mSharedPreferences.getString(SettingsEnum.ACTOR.getKey(), 53 | (String) SettingsEnum.ACTOR.getDefaultValue())); 54 | Roles role = Roles.getRole( 55 | mSharedPreferences.getString(SettingsEnum.ROLE.getKey(), 56 | (String) SettingsEnum.ROLE.getDefaultValue())); 57 | Styles style = Styles.getStyle( 58 | mSharedPreferences.getString(SettingsEnum.STYLE.getKey(), 59 | (String) SettingsEnum.STYLE.getDefaultValue())); 60 | int styleDegree = mSharedPreferences.getInt(SettingsEnum.STYLE_DEGREE.getKey(), 61 | (Integer) SettingsEnum.STYLE_DEGREE.getDefaultValue()); 62 | Ssml ssml = new Ssml(text, actor.getId(), pitch, 63 | rate, role.getId(), style.getId(), styleDegree); 64 | try { 65 | SpeechSynthesisResult speechRecognitionResult = 66 | mSpeechSynthesizer.SpeakSsmlAsync(ssml.toString()).get(); 67 | if (speechRecognitionResult.getReason() == ResultReason.SynthesizingAudioCompleted) { 68 | Log.d(TAG,"Speech synthesized to speaker for text = " + text); 69 | return true; 70 | } else if (speechRecognitionResult.getReason() == ResultReason.Canceled) { 71 | SpeechSynthesisCancellationDetails cancellation = 72 | SpeechSynthesisCancellationDetails.fromResult(speechRecognitionResult); 73 | if (cancellation.getReason() == CancellationReason.Error) { 74 | Log.e(TAG,"Speech synthesized error"); 75 | Log.e(TAG,cancellation.getErrorDetails()); 76 | } 77 | } 78 | } catch (ExecutionException | InterruptedException e) { 79 | e.printStackTrace(); 80 | } 81 | return false; 82 | } 83 | 84 | @Override 85 | public void stopSpeak() { 86 | if (mSpeechSynthesizer != null) { 87 | mSpeechSynthesizer.StopSpeakingAsync(); 88 | } 89 | } 90 | 91 | @Override 92 | public void initTts() { 93 | Log.i(TAG, "initTts"); 94 | Regions region = Regions.getRegion( 95 | mSharedPreferences.getString( 96 | SettingsEnum.AZURE_REGION.getKey(), 97 | (String) SettingsEnum.AZURE_REGION.getDefaultValue())); 98 | String token=mSharedPreferences.getString( 99 | SettingsEnum.AZURE_TOKEN.getKey(), 100 | (String) SettingsEnum.AZURE_TOKEN.getDefaultValue()); 101 | if ("".equals(token)){ 102 | token=(String) SettingsEnum.AZURE_TOKEN.getDefaultValue(); 103 | } 104 | SpeechConfig speechConfig = SpeechConfig.fromSubscription(token, region.getId()); 105 | OutputFormat outputFormat = OutputFormat.getOutputFormat( 106 | mSharedPreferences.getString( 107 | SettingsEnum.OUTPUT_FORMAT.getKey(), 108 | (String) SettingsEnum.OUTPUT_FORMAT.getDefaultValue())); 109 | speechConfig.setSpeechSynthesisOutputFormat(outputFormat.getSpeechSynthesisOutputFormat()); 110 | AudioConfig audioConfig = AudioConfig.fromDefaultSpeakerOutput(); 111 | mSpeechSynthesizer = new SpeechSynthesizer(speechConfig, audioConfig); 112 | } 113 | 114 | public static boolean testAzureConfig(String token,String region){ 115 | SpeechConfig speechConfig = SpeechConfig.fromSubscription(token, region); 116 | SpeechSynthesizer speechSynthesizer=new SpeechSynthesizer(speechConfig,null); 117 | try { 118 | SpeechSynthesisResult speechRecognitionResult = 119 | speechSynthesizer.SpeakTextAsync("").get(); 120 | if (speechRecognitionResult.getReason() == ResultReason.SynthesizingAudioCompleted) { 121 | return true; 122 | } 123 | else if (speechRecognitionResult.getReason() == ResultReason.Canceled) { 124 | SpeechSynthesisCancellationDetails cancellation = 125 | SpeechSynthesisCancellationDetails.fromResult(speechRecognitionResult); 126 | if (cancellation.getReason() == CancellationReason.Error) { 127 | return false; 128 | } 129 | } 130 | } catch (ExecutionException | InterruptedException e) { 131 | e.printStackTrace(); 132 | return false; 133 | } 134 | return false; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /app/src/main/java/com/utopiaxc/utopiatts/tts/Tts.java: -------------------------------------------------------------------------------- 1 | package com.utopiaxc.utopiatts.tts; 2 | 3 | import android.speech.tts.SynthesisCallback; 4 | 5 | public interface Tts { 6 | boolean doSpeak(String text, int pitch, int rate, SynthesisCallback synthesisCallback); 7 | void stopSpeak(); 8 | void initTts(); 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/utopiaxc/utopiatts/tts/WsTts.java: -------------------------------------------------------------------------------- 1 | package com.utopiaxc.utopiatts.tts; 2 | 3 | import static com.utopiaxc.utopiatts.tts.utils.CommonTool.getTime; 4 | 5 | import android.content.Context; 6 | import android.content.SharedPreferences; 7 | import android.media.MediaCodec; 8 | import android.media.MediaExtractor; 9 | import android.media.MediaFormat; 10 | import android.os.SystemClock; 11 | import android.speech.tts.SynthesisCallback; 12 | import android.text.TextUtils; 13 | import android.util.Log; 14 | 15 | import androidx.annotation.NonNull; 16 | import androidx.preference.PreferenceManager; 17 | 18 | import com.utopiaxc.utopiatts.APP; 19 | import com.utopiaxc.utopiatts.enums.SettingsEnum; 20 | import com.utopiaxc.utopiatts.tts.enums.Actors; 21 | import com.utopiaxc.utopiatts.tts.enums.OutputFormat; 22 | import com.utopiaxc.utopiatts.tts.enums.Regions; 23 | import com.utopiaxc.utopiatts.tts.enums.Roles; 24 | import com.utopiaxc.utopiatts.tts.enums.Styles; 25 | import com.utopiaxc.utopiatts.tts.utils.ByteArrayMediaDataSource; 26 | import com.utopiaxc.utopiatts.tts.utils.CommonTool; 27 | import com.utopiaxc.utopiatts.tts.utils.Constants; 28 | import com.utopiaxc.utopiatts.tts.utils.Ssml; 29 | import com.utopiaxc.utopiatts.tts.utils.WebSocketState; 30 | 31 | import org.jetbrains.annotations.NotNull; 32 | import org.jetbrains.annotations.Nullable; 33 | 34 | import java.io.IOException; 35 | import java.nio.ByteBuffer; 36 | import java.nio.charset.StandardCharsets; 37 | import java.util.Date; 38 | import java.util.Objects; 39 | 40 | import okhttp3.OkHttpClient; 41 | import okhttp3.Request; 42 | import okhttp3.Response; 43 | import okhttp3.WebSocket; 44 | import okhttp3.WebSocketListener; 45 | import okio.Buffer; 46 | import okio.ByteString; 47 | 48 | public class WsTts implements Tts { 49 | private static final String TAG = "WsTts"; 50 | private static volatile Tts mInstance; 51 | SharedPreferences mSharedPreferences; 52 | private volatile boolean mIsSynthesizing = false; 53 | private OutputFormat mOutputFormat = null; 54 | private Regions mRegions = null; 55 | private WebSocket mWebSocket = null; 56 | private volatile WebSocketState mWebSocketState = WebSocketState.OFFLINE; 57 | private OkHttpClient mClient; 58 | private final Buffer mData = new Buffer(); 59 | private MediaCodec mMediaCodec; 60 | private String mOldMime; 61 | private Ssml mSsml; 62 | private SynthesisCallback mCallback; 63 | private final WebSocketListener mWebSocketListener = new WebSocketListener() { 64 | @Override 65 | public void onClosed(@NotNull WebSocket webSocket, int code, @NotNull String reason) { 66 | super.onClosed(webSocket, code, reason); 67 | Log.e(TAG, "onClosed:" + reason); 68 | } 69 | 70 | @Override 71 | public void onClosing(@NotNull WebSocket webSocket, int code, @NotNull String reason) { 72 | super.onClosing(webSocket, code, reason); 73 | Log.e(TAG, "onClosing:" + reason); 74 | mWebSocket = null; 75 | mWebSocketState = WebSocketState.OFFLINE; 76 | if (mIsSynthesizing) { 77 | mWebSocket = getOrCreateWs(); 78 | } 79 | } 80 | 81 | @Override 82 | public void onFailure(@NotNull WebSocket webSocket, @NotNull Throwable t, @Nullable Response response) { 83 | super.onFailure(webSocket, t, response); 84 | mWebSocket = null; 85 | mWebSocketState = WebSocketState.OFFLINE; 86 | Log.e(TAG, "onFailure, throwable = \n", t); 87 | try { 88 | getOrCreateWs().send(mSsml.toStringForWs()); 89 | } catch (NullPointerException exception) { 90 | exception.printStackTrace(); 91 | } 92 | } 93 | 94 | @Override 95 | public void onMessage(@NotNull WebSocket webSocket, @NotNull String text) { 96 | super.onMessage(webSocket, text); 97 | final String endTag = "turn.end"; 98 | final String startTag = "turn.start"; 99 | int endIndex = text.lastIndexOf(endTag); 100 | int startIndex = text.lastIndexOf(startTag); 101 | if (startIndex != -1) { 102 | mIsSynthesizing = true; 103 | mData.clear(); 104 | } else if (endIndex != -1) { 105 | if (mCallback != null && !mCallback.hasFinished() && mIsSynthesizing) { 106 | if (mOutputFormat.needDecode()) { 107 | doDecode(mData.readByteString()); 108 | } else { 109 | doUnDecode(mData.readByteString()); 110 | } 111 | } 112 | } 113 | } 114 | 115 | @Override 116 | public void onMessage(@NotNull WebSocket webSocket, @NotNull ByteString bytes) { 117 | super.onMessage(webSocket, bytes); 118 | final String audioTag = "Path:audio\r\n"; 119 | final String startTag = "Content-Type:"; 120 | final String endTag = "\r\nX-StreamId"; 121 | int audioIndex = bytes.lastIndexOf(audioTag.getBytes(StandardCharsets.UTF_8)) + audioTag.length(); 122 | int startIndex = bytes.lastIndexOf(startTag.getBytes(StandardCharsets.UTF_8)) + startTag.length(); 123 | int endIndex = bytes.lastIndexOf(endTag.getBytes(StandardCharsets.UTF_8)); 124 | if (audioIndex != -1) { 125 | try { 126 | String temp = bytes.substring(startIndex, endIndex).utf8(); 127 | String mCurrentMime; 128 | if (temp.startsWith("audio")) { 129 | mCurrentMime = temp; 130 | } else { 131 | return; 132 | } 133 | if (!mOutputFormat.needDecode()) { 134 | if ("audio/x-wav".equals(mCurrentMime) && bytes.lastIndexOf("RIFF".getBytes(StandardCharsets.UTF_8)) != -1) { 135 | audioIndex += 44; 136 | } 137 | } 138 | mData.write(bytes.substring(audioIndex)); 139 | } catch (Exception e) { 140 | mIsSynthesizing = false; 141 | } 142 | } 143 | } 144 | 145 | @Override 146 | public void onOpen(@NotNull WebSocket webSocket, @NotNull Response response) { 147 | super.onOpen(webSocket, response); 148 | Log.e(TAG, "onOpen" + response.headers()); 149 | } 150 | }; 151 | 152 | public WsTts(Context context) { 153 | mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); 154 | initTts(); 155 | } 156 | 157 | public static Tts getInstance(Context context) { 158 | if (mInstance == null) { 159 | synchronized (Tts.class) { 160 | if (mInstance == null) { 161 | mInstance = new WsTts(context.getApplicationContext()); 162 | } 163 | } 164 | } 165 | return mInstance; 166 | } 167 | 168 | @Override 169 | public boolean doSpeak(String text, int pitch, int rate, SynthesisCallback synthesisCallback) { 170 | mCallback = synthesisCallback; 171 | mIsSynthesizing = true; 172 | if (CommonTool.isNoVoice(text)) { 173 | mIsSynthesizing = false; 174 | return true; 175 | } 176 | 177 | long startTime = SystemClock.elapsedRealtime(); 178 | synchronized (WsTts.this) { 179 | mIsSynthesizing = true; 180 | sendText(text, pitch, rate); 181 | while (mIsSynthesizing) { 182 | try { 183 | this.wait(100); 184 | } catch (InterruptedException e) { 185 | e.printStackTrace(); 186 | } 187 | long time = SystemClock.elapsedRealtime() - startTime; 188 | if (time > 50000) { 189 | return false; 190 | } 191 | } 192 | } 193 | mIsSynthesizing = false; 194 | return true; 195 | } 196 | 197 | @Override 198 | public void stopSpeak() { 199 | if (mWebSocket != null) { 200 | Objects.requireNonNull(mWebSocket).close(1000, "closed by call onStop"); 201 | mWebSocket = null; 202 | } 203 | mIsSynthesizing = false; 204 | mData.clear(); 205 | } 206 | 207 | @Override 208 | public void initTts() { 209 | Log.i(TAG, "initTts"); 210 | stopSpeak(); 211 | if (mCallback != null) { 212 | mCallback.error(); 213 | } 214 | mRegions = Regions.getRegion( 215 | mSharedPreferences.getString( 216 | SettingsEnum.AZURE_REGION.getKey(), 217 | (String) SettingsEnum.AZURE_REGION.getDefaultValue())); 218 | 219 | mOutputFormat = OutputFormat.getOutputFormat( 220 | mSharedPreferences.getString( 221 | SettingsEnum.OUTPUT_FORMAT.getKey(), 222 | (String) SettingsEnum.OUTPUT_FORMAT.getDefaultValue())); 223 | mClient = APP.getOkHttpClient(); 224 | mWebSocket = getOrCreateWs(); 225 | sendConfig(mWebSocket); 226 | } 227 | 228 | public synchronized void sendText(String text, int pitch, int rate) { 229 | Actors actor = Actors.getActor( 230 | mSharedPreferences.getString(SettingsEnum.ACTOR.getKey(), 231 | (String) SettingsEnum.ACTOR.getDefaultValue())); 232 | Roles role = Roles.getRole( 233 | mSharedPreferences.getString(SettingsEnum.ROLE.getKey(), 234 | (String) SettingsEnum.ROLE.getDefaultValue())); 235 | Styles style = Styles.getStyle( 236 | mSharedPreferences.getString(SettingsEnum.STYLE.getKey(), 237 | (String) SettingsEnum.STYLE.getDefaultValue())); 238 | int styleDegree = mSharedPreferences.getInt(SettingsEnum.STYLE_DEGREE.getKey(), 239 | (Integer) SettingsEnum.STYLE_DEGREE.getDefaultValue()); 240 | 241 | mSsml = new Ssml(text, actor.getId(), pitch, 242 | rate, role.getId(), style.getId(), styleDegree); 243 | 244 | while (mIsSynthesizing) { 245 | Log.w(TAG, "try sendText"); 246 | try { 247 | if (getOrCreateWs().send(mSsml.toStringForWs())) { 248 | break; 249 | } 250 | } catch (Exception e) { 251 | try { 252 | this.wait(500); 253 | } catch (Exception ignored) { 254 | } 255 | Log.w(TAG, "Retry sendText"); 256 | } 257 | } 258 | } 259 | 260 | private synchronized void sendConfig(@NonNull WebSocket ws) { 261 | String msg = "X-Timestamp:+" + getTime() + "\r\n" + 262 | "Content-Type:application/json; charset=utf-8\r\n" + 263 | "Path:speech.config\r\n\r\n" 264 | + "{\"context\":" + 265 | "{\"synthesis\":" + 266 | "{\"audio\":" + 267 | "{\"metadataoptions\":" + 268 | "{\"sentenceBoundaryEnabled\":\"true\",\"wordBoundaryEnabled\":\"true\"" + 269 | "},\"outputFormat\":\"" + mOutputFormat.getValue() + "\"}}}}"; 270 | ws.send(msg); 271 | } 272 | 273 | public synchronized WebSocket getOrCreateWs() { 274 | if (mWebSocket == null) { 275 | if (mWebSocketState == WebSocketState.CONNECTED) { 276 | mClient.dispatcher().cancelAll(); 277 | } 278 | String url; 279 | String origin; 280 | url = Constants.WSS + mRegions.getId() + Constants.MS_API + 281 | CommonTool.getMD5String(new Date().toString()); 282 | origin = "https://azure.microsoft.com"; 283 | Request request = new Request.Builder() 284 | .url(url) 285 | .header("User-Agent", Constants.EDGE_UA) 286 | .addHeader("Origin", origin) 287 | .build(); 288 | mWebSocketState = WebSocketState.CONNECTING; 289 | mWebSocket = mClient.newWebSocket(request, mWebSocketListener); 290 | 291 | mWebSocketState = WebSocketState.CONNECTED; 292 | sendConfig(Objects.requireNonNull(mWebSocket)); 293 | } 294 | return mWebSocket; 295 | } 296 | 297 | private synchronized void doDecode(@NonNull ByteString data) { 298 | mIsSynthesizing = true; 299 | try { 300 | MediaExtractor mediaExtractor = new MediaExtractor(); 301 | mediaExtractor.setDataSource(new ByteArrayMediaDataSource(data.toByteArray())); 302 | int audioTrackIndex = -1; 303 | String mime = null; 304 | MediaFormat trackFormat = null; 305 | for (int i = 0; i < mediaExtractor.getTrackCount(); i++) { 306 | trackFormat = mediaExtractor.getTrackFormat(i); 307 | mime = trackFormat.getString(MediaFormat.KEY_MIME); 308 | if (!TextUtils.isEmpty(mime) && mime.startsWith("audio")) { 309 | audioTrackIndex = i; 310 | break; 311 | } 312 | } 313 | if (audioTrackIndex == -1) { 314 | Log.e(TAG, "initAudioDecoder: 没有找到音频流"); 315 | mIsSynthesizing = false; 316 | return; 317 | } 318 | if ("audio/opus".equals(mime)) { 319 | Buffer buf = new Buffer(); 320 | buf.write("OpusHead".getBytes(StandardCharsets.UTF_8)); 321 | buf.writeByte(1); 322 | buf.writeByte(1); 323 | buf.writeShortLe(0); 324 | buf.writeIntLe(mOutputFormat.getSoundFrequency()); 325 | buf.writeShortLe(0); 326 | buf.writeByte(0); 327 | byte[] csd1bytes = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; 328 | byte[] csd2bytes = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; 329 | ByteString hd = buf.readByteString(); 330 | ByteBuffer csd0 = ByteBuffer.wrap(hd.toByteArray()); 331 | trackFormat.setByteBuffer("csd-0", csd0); 332 | ByteBuffer csd1 = ByteBuffer.wrap(csd1bytes); 333 | trackFormat.setByteBuffer("csd-1", csd1); 334 | ByteBuffer csd2 = ByteBuffer.wrap(csd2bytes); 335 | trackFormat.setByteBuffer("csd-2", csd2); 336 | } 337 | mediaExtractor.selectTrack(audioTrackIndex); 338 | MediaCodec mediaCodec = getMediaCodec(mime, trackFormat); 339 | mediaCodec.start(); 340 | MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); 341 | ByteBuffer inputBuffer; 342 | long TIME_OUT_US = 10000; 343 | while (mIsSynthesizing) { 344 | int inputIndex = mediaCodec.dequeueInputBuffer(TIME_OUT_US); 345 | if (inputIndex < 0) { 346 | break; 347 | } 348 | bufferInfo.presentationTimeUs = mediaExtractor.getSampleTime(); 349 | inputBuffer = mediaCodec.getInputBuffer(inputIndex); 350 | if (inputBuffer != null) { 351 | inputBuffer.clear(); 352 | } else { 353 | continue; 354 | } 355 | int sampleSize = mediaExtractor.readSampleData(inputBuffer, 0); 356 | 357 | if (sampleSize > 0) { 358 | bufferInfo.size = sampleSize; 359 | mediaCodec.queueInputBuffer(inputIndex, 0, sampleSize, 0, 0); 360 | mediaExtractor.advance(); 361 | } else { 362 | break; 363 | } 364 | int outputIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, TIME_OUT_US); 365 | ByteBuffer outputBuffer; 366 | byte[] pcmData; 367 | while (outputIndex >= 0) { 368 | outputBuffer = mediaCodec.getOutputBuffer(outputIndex); 369 | pcmData = new byte[bufferInfo.size]; 370 | if (outputBuffer != null) { 371 | outputBuffer.get(pcmData); 372 | outputBuffer.clear(); 373 | } 374 | mCallback.audioAvailable(pcmData, 0, bufferInfo.size); 375 | mediaCodec.releaseOutputBuffer(outputIndex, false); 376 | outputIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, TIME_OUT_US); 377 | } 378 | } 379 | mediaCodec.reset(); 380 | mIsSynthesizing = false; 381 | } catch (Exception e) { 382 | Log.e(TAG, "doDecode", e); 383 | mIsSynthesizing = false; 384 | } 385 | } 386 | 387 | 388 | private synchronized void doUnDecode(@NonNull ByteString data) { 389 | mIsSynthesizing = true; 390 | int length = data.toByteArray().length; 391 | final int maxBufferSize = mCallback.getMaxBufferSize(); 392 | int offset = 0; 393 | while (offset < length && mIsSynthesizing) { 394 | int bytesToWrite = Math.min(maxBufferSize, length - offset); 395 | mCallback.audioAvailable(data.toByteArray(), offset, bytesToWrite); 396 | offset += bytesToWrite; 397 | } 398 | mCallback.done(); 399 | mIsSynthesizing = false; 400 | } 401 | 402 | private MediaCodec getMediaCodec(String mime, MediaFormat mediaFormat) { 403 | if (mMediaCodec == null || !mime.equals(mOldMime)) { 404 | if (null != mMediaCodec) { 405 | mMediaCodec.release(); 406 | } 407 | try { 408 | mMediaCodec = MediaCodec.createDecoderByType(mime); 409 | mOldMime = mime; 410 | } catch (IOException ioException) { 411 | ioException.printStackTrace(); 412 | throw new RuntimeException(ioException); 413 | } 414 | } 415 | mMediaCodec.reset(); 416 | mMediaCodec.configure(mediaFormat, null, null, 0); 417 | return mMediaCodec; 418 | } 419 | } 420 | -------------------------------------------------------------------------------- /app/src/main/java/com/utopiaxc/utopiatts/tts/enums/Actors.java: -------------------------------------------------------------------------------- 1 | package com.utopiaxc.utopiatts.tts.enums; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | public enum Actors { 6 | ZH_CN_XIAOCHEN_NEURAL("ZH_CN_XIAOCHEN_NEURAL", "zh-CN", "zh-CN-XiaochenNeural", 'F',false), 7 | ZH_CN_XIAOHAN_NEURAL("ZH_CN_XIAOHAN_NEURAL", "zh-CN", "zh-CN-XiaohanNeural", 'F',false), 8 | ZH_CN_XIAOMENG_NEURAL("ZH_CN_XIAOMENG_NEURAL", "zh-CN", "zh-CN-XiaomengNeural", 'F',true), 9 | ZH_CN_XIAOMO_NEURAL("ZH_CN_XIAOMO_NEURAL", "zh-CN", "zh-CN-XiaomoNeural", 'F',false), 10 | ZH_CN_XIAOQIU_NEURAL("ZH_CN_XIAOQIU_NEURAL", "zh-CN", "zh-CN-XiaoqiuNeural", 'F',false), 11 | ZH_CN_XIAORUI_NEURAL("ZH_CN_XIAORUI_NEURAL", "zh-CN", "zh-CN-XiaoruiNeural", 'F',false), 12 | ZH_CN_XIAOSHUANG_NEURAL("ZH_CN_XIAOSHUANG_NEURAL", "zh-CN", "zh-CN-XiaoshuangNeural", 'F',false), 13 | ZH_CN_XIAOXIAO_NEURAL("ZH_CN_XIAOXIAO_NEURAL", "zh-CN", "zh-CN-XiaoxiaoNeural", 'F',false), 14 | ZH_CN_XIAOXUAN_NEURAL("ZH_CN_XIAOXUAN_NEURAL", "zh-CN", "zh-CN-XiaoxuanNeural", 'F',false), 15 | ZH_CN_XIAOYAN_NEURAL("ZH_CN_XIAOYAN_NEURAL", "zh-CN", "zh-CN-XiaoyanNeural", 'F',false), 16 | ZH_CN_XIAOYI_NEURAL("ZH_CN_XIAOYI_NEURAL", "zh-CN", "zh-CN-XiaoyiNeural", 'F',true), 17 | ZH_CN_XIAOYOU_NEURAL("ZH_CN_XIAOYOU_NEURAL", "zh-CN", "zh-CN-XiaoyouNeural", 'F',false), 18 | ZH_CN_XIAOZHEN_NEURAL("ZH_CN_XIAOZHEN_NEURAL", "zh-CN", "zh-CN-XiaozhenNeural", 'F',true), 19 | ZH_CN_YUNFENG_NEURAL("ZH_CN_YUNFENG_NEURAL", "zh-CN", "zh-CN-YunfengNeural", 'M',true), 20 | ZH_CN_YUNHAO_NEURAL("ZH_CN_YUNHAO_NEURAL", "zh-CN", "zh-CN-YunhaoNeural", 'M',true), 21 | ZH_CN_YUNJIAN_NEURAL("ZH_CN_YUNJIAN_NEURAL", "zh-CN", "zh-CN-YunjianNeural", 'M',true), 22 | ZH_CN_YUNXIA_NEURAL("ZH_CN_YUNXIA_NEURAL", "zh-CN", "zh-CN-YunxiaNeural", 'M',true), 23 | ZH_CN_YUNXI_NEURAL("ZH_CN_YUNXI_NEURAL", "zh-CN", "zh-CN-YunxiNeural", 'M',false), 24 | ZH_CN_YUNYANG_NEURAL("ZH_CN_YUNYANG_NEURAL", "zh-CN", "zh-CN-YunyangNeural", 'M',false), 25 | ZH_CN_YUNYE_NEURAL("ZH_CN_YUNYE_NEURAL", "zh-CN", "zh-CN-YunyeNeural", 'M',false), 26 | ZH_CN_YUNZE_NEURAL("ZH_CN_YUNZE_NEURAL", "zh-CN", "zh-CN-YunzeNeural", 'M',true), 27 | ZH_CN_HENAN_YUNDENG_NEURAL("ZH_CN_HENAN_YUNDENG_NEURAL", "zh-CN-henan", "zh-CN-henan-YundengNeural", 'M',true), 28 | ZH_CN_LIAONING_XIAOBEI_NEURAL("ZH_CN_LIAONING_XIAOBEI_NEURAL", "zh-CN-liaoning", "zh-CN-liaoning-XiaobeiNeural", 'F',true), 29 | ZH_CN_SHAANXI_XIAONI_NEURAL("ZH_CN_SHAANXI_XIAONI_NEURAL", "zh-CN-shaanxi", "zh-CN-shaanxi-XiaoniNeural", 'F',true), 30 | ZH_CN_SHANDONG_YUNXIANG_NEURAL("ZH_CN_SHANDONG_YUNXIANG_NEURAL", "zh-CN-shandong", "zh-CN-shandong-YunxiangNeural", 'M',true), 31 | ZH_CN_SICHUAN_YUNXI_NEURAL("ZH_CN_SICHUAN_YUNXI_NEURAL", "zh-CN-sichuan", "zh-CN-sichuan-YunxiNeural", 'M',true), 32 | ZH_HK_HIUGAAI_NEURAL("ZH_HK_HIUGAAI_NEURAL", "zh-HK", "zh-HK-HiuGaaiNeural", 'F',false), 33 | ZH_HK_HIUMAAN_NEURAL("ZH_HK_HIUMAAN_NEURAL", "zh-HK", "zh-HK-HiuMaanNeural", 'F',false), 34 | ZH_HK_WANLUNG_NEURAL("ZH_HK_WANLUNG_NEURAL", "zh-HK", "zh-HK-WanLungNeural", 'M',false), 35 | ZH_TW_HSIAOCHEN_NEURAL("ZH_TW_HSIAOCHEN_NEURAL", "zh-TW", "zh-TW-HsiaoChenNeural", 'F',false), 36 | ZH_TW_HSIAOYU_NEURAL("ZH_TW_HSIAOYU_NEURAL", "zh-TW", "zh-TW-HsiaoYuNeural", 'F',false), 37 | ZH_TW_YUNJHE_NEURAL("ZH_TW_YUNJHE_NEURAL", "zh-TW", "zh-TW-YunJheNeural", 'M',false); 38 | 39 | private final String mName; 40 | private final String mRegion; 41 | private final String mId; 42 | private final Character mGender; 43 | private final boolean mIsPre; 44 | 45 | Actors(String name, String region, String id, Character gender, boolean isPre) { 46 | mName = name; 47 | mRegion = region; 48 | mId = id; 49 | mGender = gender; 50 | mIsPre = isPre; 51 | } 52 | 53 | @NonNull 54 | @Override 55 | public String toString() { 56 | return mName; 57 | } 58 | 59 | 60 | public static Actors getActor(String name) { 61 | for (Actors actors : Actors.values()) { 62 | if (actors.getName().equals(name)) { 63 | return actors; 64 | } 65 | } 66 | return ZH_CN_XIAOXIAO_NEURAL; 67 | } 68 | 69 | public String getName() { 70 | return mName; 71 | } 72 | 73 | public String getId() { 74 | return mId; 75 | } 76 | 77 | public String getRegion() { 78 | return mRegion; 79 | } 80 | 81 | public Character getGender() { 82 | return mGender; 83 | } 84 | 85 | public boolean isFemale() { 86 | return mGender == 'F'; 87 | } 88 | 89 | public boolean isPre(){ 90 | return mIsPre; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /app/src/main/java/com/utopiaxc/utopiatts/tts/enums/Driver.java: -------------------------------------------------------------------------------- 1 | package com.utopiaxc.utopiatts.tts.enums; 2 | 3 | public enum Driver { 4 | AZURE_SDK("AZURE_SDK","azure_sdk"), 5 | WEBSOCKET("WEBSOCKET","websocket"); 6 | 7 | private final String mName; 8 | private final String mId; 9 | 10 | Driver(String name, String id){ 11 | mName=name; 12 | mId=id; 13 | } 14 | 15 | public String getName() { 16 | return mName; 17 | } 18 | 19 | public String getId() { 20 | return mId; 21 | } 22 | 23 | public static Driver getRole(String name) { 24 | for (Driver role: Driver.values()) { 25 | if (role.getName().equals(name)) { 26 | return role; 27 | } 28 | } 29 | return AZURE_SDK; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/utopiaxc/utopiatts/tts/enums/OutputFormat.java: -------------------------------------------------------------------------------- 1 | package com.utopiaxc.utopiatts.tts.enums; 2 | 3 | import android.media.AudioFormat; 4 | 5 | import androidx.annotation.NonNull; 6 | 7 | import com.microsoft.cognitiveservices.speech.SpeechSynthesisOutputFormat; 8 | 9 | public enum OutputFormat { 10 | AUDIO_16K_HZ_128K_BIT_RATE_MONO_MP3("AUDIO_16K_HZ_128K_BIT_RATE_MONO_MP3", 11 | "audio-16khz-128kbitrate-mono-mp3", 12 | true, 13 | 16000, AudioFormat.ENCODING_PCM_16BIT, 14 | SpeechSynthesisOutputFormat.Audio16Khz128KBitRateMonoMp3), 15 | AUDIO_16K_HZ_16_BIT_32KBPS_MONO_OPUS("AUDIO_16K_HZ_128K_BIT_RATE_MONO_MP3", 16 | "audio-16khz-128kbitrate-mono-mp3", 17 | true, 18 | 16000, AudioFormat.ENCODING_PCM_16BIT, 19 | SpeechSynthesisOutputFormat.Audio16Khz16Bit32KbpsMonoOpus), 20 | AUDIO_16K_HZ_16KBPS_MONO_SIREN("AUDIO_16K_HZ_16KBPS_MONO_SIREN", 21 | "audio-16khz-16kbps-mono-siren", 22 | false, 23 | 16000, AudioFormat.ENCODING_PCM_16BIT, 24 | SpeechSynthesisOutputFormat.Audio16Khz16KbpsMonoSiren), 25 | AUDIO_16K_HZ_32K_BIT_RATE_MONO_MP3("AUDIO_16K_HZ_32K_BIT_RATE_MONO_MP3", 26 | "audio-16khz-32kbitrate-mono-mp3", 27 | true, 28 | 16000, AudioFormat.ENCODING_PCM_16BIT, 29 | SpeechSynthesisOutputFormat.Audio16Khz32KBitRateMonoMp3), 30 | AUDIO_16K_HZ_64K_BIT_RATE_MONO_MP3("AUDIO_16K_HZ_64K_BIT_RATE_MONO_MP3", 31 | "audio-16khz-64kbitrate-mono-mp3", 32 | true, 33 | 16000, AudioFormat.ENCODING_PCM_16BIT, 34 | SpeechSynthesisOutputFormat.Audio16Khz64KBitRateMonoMp3), 35 | AUDIO_24K_HZ_160K_BIT_RATE_MONO_MP3("AUDIO_24K_HZ_160K_BIT_RATE_MONO_MP3", 36 | "audio-24khz-160kbitrate-mono-mp3", 37 | true, 38 | 24000, AudioFormat.ENCODING_PCM_16BIT, 39 | SpeechSynthesisOutputFormat.Audio24Khz160KBitRateMonoMp3), 40 | AUDIO_24K_HZ_16_BIT_24KBPS_MONO_OPUS("AUDIO_24K_HZ_16_BIT_24KBPS_MONO_OPUS", 41 | "audio-24khz-16bit-24kbps-mono-opus", 42 | true, 43 | 24000, AudioFormat.ENCODING_PCM_16BIT, 44 | SpeechSynthesisOutputFormat.Audio24Khz16Bit24KbpsMonoOpus), 45 | AUDIO_24K_HZ_16_BIT_48KBPS_MONO_OPUS("AUDIO_24K_HZ_16_BIT_48KBPS_MONO_OPUS", 46 | "audio-24khz-16bit-48kbps-mono-opus", 47 | true, 48 | 24000, AudioFormat.ENCODING_PCM_16BIT, 49 | SpeechSynthesisOutputFormat.Audio24Khz16Bit48KbpsMonoOpus), 50 | AUDIO_24K_HZ_48K_BIT_RATE_MONO_MP3("AUDIO_24K_HZ_48K_BIT_RATE_MONO_MP3", 51 | "audio-24khz-48kbitrate-mono-mp3", 52 | true, 53 | 24000, AudioFormat.ENCODING_PCM_16BIT, 54 | SpeechSynthesisOutputFormat.Audio24Khz48KBitRateMonoMp3), 55 | AUDIO_24K_HZ_96K_BIT_RATE_MONO_MP3("AUDIO_24K_HZ_96K_BIT_RATE_MONO_MP3", 56 | "audio-24khz-96kbitrate-mono-mp3", 57 | true, 58 | 24000, AudioFormat.ENCODING_PCM_16BIT, 59 | SpeechSynthesisOutputFormat.Audio24Khz96KBitRateMonoMp3), 60 | AUDIO_48K_HZ_192K_BIT_RATE_MONO_MP3("AUDIO_48K_HZ_192K_BIT_RATE_MONO_MP3", 61 | "audio-48khz-192kbitrate-mono-mp3", 62 | true, 63 | 48000, AudioFormat.ENCODING_PCM_16BIT, 64 | SpeechSynthesisOutputFormat.Audio48Khz192KBitRateMonoMp3), 65 | AUDIO_48K_HZ_96K_BIT_RATE_MONO_MP3("AUDIO_48K_HZ_96K_BIT_RATE_MONO_MP3", 66 | "audio-48khz-96kbitrate-mono-mp3", 67 | true, 68 | 48000, AudioFormat.ENCODING_PCM_16BIT, 69 | SpeechSynthesisOutputFormat.Audio48Khz192KBitRateMonoMp3), 70 | OGG_16K_HZ_16_BIT_MONO_OPUS("OGG_16K_HZ_16_BIT_MONO_OPUS", 71 | "ogg-16khz-16bit-mono-opus", 72 | true, 73 | 16000, AudioFormat.ENCODING_PCM_16BIT, 74 | SpeechSynthesisOutputFormat.Ogg16Khz16BitMonoOpus), 75 | OGG_24K_HZ_16_BIT_MONO_OPUS("OGG_24K_HZ_16_BIT_MONO_OPUS", 76 | "ogg-24khz-16bit-mono-opus", 77 | true, 78 | 24000, AudioFormat.ENCODING_PCM_16BIT, 79 | SpeechSynthesisOutputFormat.Ogg24Khz16BitMonoOpus), 80 | OGG_48K_HZ_16_BIT_MONO_OPUS("OGG_48K_HZ_16_BIT_MONO_OPUS", 81 | "ogg-48khz-16bit-mono-opus", 82 | true, 83 | 48000, AudioFormat.ENCODING_PCM_16BIT, 84 | SpeechSynthesisOutputFormat.Ogg48Khz16BitMonoOpus), 85 | RAW_16K_HZ_16_BIT_MONO_PCM("RAW_16K_HZ_16_BIT_MONO_PCM", 86 | "raw-16khz-16bit-mono-pcm", 87 | false, 88 | 16000, AudioFormat.ENCODING_PCM_16BIT, 89 | SpeechSynthesisOutputFormat.Raw16Khz16BitMonoPcm), 90 | RAW_16K_HZ_16_BIT_MONO_TRUE_SILK("RAW_16K_HZ_16_BIT_MONO_TRUE_SILK", 91 | "raw-16khz-16bit-mono-true-silk", 92 | false, 93 | 16000, AudioFormat.ENCODING_PCM_16BIT, 94 | SpeechSynthesisOutputFormat.Raw16Khz16BitMonoTrueSilk), 95 | RAW_22050_HZ_16_BIT_MONO_PCM("RAW_22050_HZ_16_BIT_MONO_PCM", 96 | "raw-22050hz-16bit-mono-pcm", 97 | false, 98 | 22050, AudioFormat.ENCODING_PCM_16BIT, 99 | SpeechSynthesisOutputFormat.Raw22050Hz16BitMonoPcm), 100 | RAW_24K_HZ_16_BIT_MONO_PCM("RAW_24K_HZ_16_BIT_MONO_PCM", 101 | "raw-24khz-16bit-mono-pcm", 102 | false, 103 | 24000, AudioFormat.ENCODING_PCM_16BIT, 104 | SpeechSynthesisOutputFormat.Raw24Khz16BitMonoPcm), 105 | RAW_24K_HZ_16_BIT_MONO_TRUE_SILK("RAW_24K_HZ_16_BIT_MONO_TRUE_SILK", 106 | "raw-24khz-16bit-mono-true-silk", 107 | false, 108 | 24000, AudioFormat.ENCODING_PCM_16BIT, 109 | SpeechSynthesisOutputFormat.Raw24Khz16BitMonoTrueSilk), 110 | RAW_44100_HZ_16_BIT_MONO_PCM("RAW_44100_HZ_16_BIT_MONO_PCM", 111 | "raw-44100hz-16bit-mono-pcm", 112 | false, 113 | 44100, AudioFormat.ENCODING_PCM_16BIT, 114 | SpeechSynthesisOutputFormat.Raw44100Hz16BitMonoPcm), 115 | RAW_48K_HZ_16_BIT_MONO_PCM("RAW_48K_HZ_16_BIT_MONO_PCM", 116 | "raw-48khz-16bit-mono-pcm", 117 | false, 118 | 48000, AudioFormat.ENCODING_PCM_16BIT, 119 | SpeechSynthesisOutputFormat.Raw48Khz16BitMonoPcm), 120 | RAW_8K_HZ_16_BIT_MONO_PCM("RAW_8K_HZ_16_BIT_MONO_PCM", 121 | "raw-8khz-16bit-mono-pcm", 122 | false, 123 | 8000, AudioFormat.ENCODING_PCM_16BIT, 124 | SpeechSynthesisOutputFormat.Raw8Khz16BitMonoPcm), 125 | RAW_8K_HZ_8_BIT_MONO_ALAW("RAW_8K_HZ_8_BIT_MONO_ALAW", 126 | "raw-8khz-8bit-mono-alaw", 127 | false, 128 | 8000, AudioFormat.ENCODING_PCM_8BIT, 129 | SpeechSynthesisOutputFormat.Raw8Khz8BitMonoALaw), 130 | RAW_8K_HZ_8_BIT_MONO_MULAW("RAW_8K_HZ_8_BIT_MONO_MULAW", 131 | "raw-8khz-8bit-mono-mulaw", 132 | false, 133 | 8000, AudioFormat.ENCODING_PCM_8BIT, 134 | SpeechSynthesisOutputFormat.Raw8Khz8BitMonoMULaw), 135 | RIFF_16K_HZ_16_BIT_MONO_PCM("RIFF_16K_HZ_16_BIT_MONO_PCM", 136 | "riff-16khz-16bit-mono-pcm", 137 | false, 138 | 16000, AudioFormat.ENCODING_PCM_16BIT, 139 | SpeechSynthesisOutputFormat.Riff16Khz16BitMonoPcm), 140 | RIFF_16K_HZ_16KBPS_MONO_SIREN("RIFF_16K_HZ_16KBPS_MONO_SIREN", 141 | "riff-16khz-16kbps-mono-siren", 142 | false, 143 | 16000, AudioFormat.ENCODING_PCM_16BIT, 144 | SpeechSynthesisOutputFormat.Riff16Khz16KbpsMonoSiren), 145 | RIFF_22050_HZ_16_BIT_MONO_PCM("RIFF_22050_HZ_16_BIT_MONO_PCM", 146 | "riff-22050hz-16bit-mono-pcm", 147 | false, 148 | 22050, AudioFormat.ENCODING_PCM_16BIT, 149 | SpeechSynthesisOutputFormat.Riff22050Hz16BitMonoPcm), 150 | RIFF_24K_HZ_16_BIT_MONO_PCM("RIFF_24K_HZ_16_BIT_MONO_PCM", 151 | "riff-24khz-16bit-mono-pcm", 152 | false, 153 | 24000, AudioFormat.ENCODING_PCM_16BIT, 154 | SpeechSynthesisOutputFormat.Riff24Khz16BitMonoPcm), 155 | RIFF_44100_HZ_16_BIT_MONO_PCM("RIFF_44100_HZ_16_BIT_MONO_PCM", 156 | "riff-44100hz-16bit-mono-pcm", 157 | false, 158 | 44100, AudioFormat.ENCODING_PCM_16BIT, 159 | SpeechSynthesisOutputFormat.Riff44100Hz16BitMonoPcm), 160 | RIFF_48K_HZ_16_BIT_MONO_PCM("RIFF_48K_HZ_16_BIT_MONO_PCM", 161 | "riff-48khz-16bit-mono-pcm", 162 | false, 163 | 48000, AudioFormat.ENCODING_PCM_16BIT, 164 | SpeechSynthesisOutputFormat.Riff48Khz16BitMonoPcm), 165 | RIFF_8K_HZ_16_BIT_MONO_PCM("RIFF_8K_HZ_16_BIT_MONO_PCM", 166 | "riff-8khz-16bit-mono-pcm", 167 | false, 168 | 8000, AudioFormat.ENCODING_PCM_16BIT, 169 | SpeechSynthesisOutputFormat.Riff8Khz16BitMonoPcm), 170 | RIFF_8K_HZ_8_BIT_MONO_ALAW("RIFF_8K_HZ_8_BIT_MONO_ALAW", 171 | "riff-8khz-8bit-mono-alaw", 172 | false, 173 | 8000, AudioFormat.ENCODING_PCM_8BIT, 174 | SpeechSynthesisOutputFormat.Riff8Khz8BitMonoALaw), 175 | RIFF_8K_HZ_8_BIT_MONO_MULAW("RIFF_8K_HZ_8_BIT_MONO_MULAW", 176 | "riff-8khz-8bit-mono-mulaw", 177 | false, 178 | 8000, AudioFormat.ENCODING_PCM_8BIT, 179 | SpeechSynthesisOutputFormat.Riff8Khz8BitMonoMULaw), 180 | WEBM_16K_HZ_16_BIT_MONO_OPUS("WEBM_16K_HZ_16_BIT_MONO_OPUS", 181 | "webm-16khz-16bit-mono-opus", 182 | true, 183 | 16000, AudioFormat.ENCODING_PCM_16BIT, 184 | SpeechSynthesisOutputFormat.Webm16Khz16BitMonoOpus), 185 | WEBM_24K_HZ_16_BIT_24KBPS_MONO_OPUS("WEBM_24K_HZ_16_BIT_24KBPS_MONO_OPUS", 186 | "webm-24khz-16bit-24kbps-mono-opus", 187 | true, 188 | 24000, AudioFormat.ENCODING_PCM_16BIT, 189 | SpeechSynthesisOutputFormat.Webm24Khz16Bit24KbpsMonoOpus), 190 | WEBM_24K_HZ_16_BIT_MONO_OPUS("WEBM_24K_HZ_16_BIT_MONO_OPUS", 191 | "webm-24khz-16bit-mono-opus", 192 | true, 193 | 24000, AudioFormat.ENCODING_PCM_16BIT, 194 | SpeechSynthesisOutputFormat.Webm24Khz16BitMonoOpus), 195 | ; 196 | 197 | private final String mName; 198 | private final int mSoundFrequency; 199 | private final int mAudioFormat; 200 | private final SpeechSynthesisOutputFormat mSpeechSynthesisOutputFormat; 201 | private final String mValue; 202 | private final boolean mNeedDecode; 203 | 204 | OutputFormat(String name, String value, boolean needDecode, int soundFrequency, int audioFormat, 205 | SpeechSynthesisOutputFormat speechSynthesisOutputFormat) { 206 | mName = name; 207 | mValue = value; 208 | mNeedDecode = needDecode; 209 | mSoundFrequency = soundFrequency; 210 | mAudioFormat = audioFormat; 211 | mSpeechSynthesisOutputFormat = speechSynthesisOutputFormat; 212 | } 213 | 214 | @NonNull 215 | @Override 216 | public String toString() { 217 | return mName; 218 | } 219 | 220 | public String getName() { 221 | return mName; 222 | } 223 | 224 | public String getValue() { 225 | return mValue; 226 | } 227 | 228 | public int getSoundFrequency() { 229 | return mSoundFrequency; 230 | } 231 | 232 | public int getAudioFormat() { 233 | return mAudioFormat; 234 | } 235 | 236 | public SpeechSynthesisOutputFormat getSpeechSynthesisOutputFormat() { 237 | return mSpeechSynthesisOutputFormat; 238 | } 239 | 240 | public boolean needDecode(){ 241 | return mNeedDecode; 242 | } 243 | 244 | public static OutputFormat getOutputFormat(String name) { 245 | for (OutputFormat outputFormat : OutputFormat.values()) { 246 | if (outputFormat.getName().equals(name)) { 247 | return outputFormat; 248 | } 249 | } 250 | return OGG_48K_HZ_16_BIT_MONO_OPUS; 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /app/src/main/java/com/utopiaxc/utopiatts/tts/enums/Regions.java: -------------------------------------------------------------------------------- 1 | package com.utopiaxc.utopiatts.tts.enums; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | public enum Regions { 6 | SOUTH_AFRICA_NORTH("SOUTH_AFRICA_NORTH", "southafricanorth", false), 7 | EAST_ASIA("EAST_ASIA", "eastasia", false), 8 | SOUTH_EAST_ASIA("SOUTH_EAST_ASIA", "southeastasia", true), 9 | AUSTRALIA_EAST("AUSTRALIA_EAST", "australiaeast", false), 10 | CENTRAL_INDIA("CENTRAL_INDIA", "centralindia", false), 11 | JAPAN_EAST("JAPAN_EAST", "japaneast", false), 12 | JAPAN_WEST("JAPAN_WEST", "japanwest", false), 13 | KOREA_CENTRAL("KOREA_CENTRAL", "koreacentral", false), 14 | CANADA_CENTRAL("CANADA_CENTRAL", "canadacentral", false), 15 | NORTH_EUROPE("NORTH_EUROPE", "northeurope", false), 16 | WEST_EUROPE("WEST_EUROPE", "westeurope", true), 17 | FRANCE_CENTRAL("FRANCE_CENTRAL", "francecentral", false), 18 | GERMANY_WEST_CENTRAL("GERMANY_WEST_CENTRAL", "germanywestcentral", false), 19 | NORWAY_EAST("NORWAY_EAST", "norwayeast", false), 20 | SWITZERLAND_NORTH("SWITZERLAND_NORTH", "switzerlandnorth", false), 21 | SWITZERLAND_WEST("SWITZERLAND_WEST", "switzerlandwest", false), 22 | UK_SOUTH("UK_SOUTH", "uksouth", false), 23 | UAE_NORTH("UAE_NORTH", "uaenorth", false), 24 | BRAZIL_SOUTH("BRAZIL_SOUTH", "brazilsouth", false), 25 | CENTRAL_US("CENTRAL_US", "centralus", false), 26 | EAST_US("EAST_US", "eastus", true), 27 | EAST_US2("EAST_US2", "eastus2", true), 28 | NORTH_CENTRAL_US("NORTH_CENTRAL_US", "northcentralus", false), 29 | SOUTH_CENTRAL_US("SOUTH_CENTRAL_US", "southcentralus", false), 30 | WEST_CENTRAL_US("WEST_CENTRAL_US", "westcentralus",false), 31 | WEST_US("WEST_US", "westus", false), 32 | WEST_US2("WEST_US2", "westus2", false), 33 | WEST_US3("WEST_US3", "westus3", false); 34 | 35 | private final String mName; 36 | private final String mId; 37 | private final boolean mIsSupportPre; 38 | 39 | Regions(String name, String id, boolean isSupportPre) { 40 | mName = name; 41 | mId = id; 42 | mIsSupportPre = isSupportPre; 43 | } 44 | 45 | @NonNull 46 | @Override 47 | public String toString() { 48 | return mName; 49 | } 50 | 51 | 52 | public static Regions getRegion(String name) { 53 | for (Regions region : Regions.values()) { 54 | if (region.getName().equals(name)) { 55 | return region; 56 | } 57 | } 58 | return SOUTH_EAST_ASIA; 59 | } 60 | 61 | public String getName() { 62 | return mName; 63 | } 64 | 65 | public String getId() { 66 | return mId; 67 | } 68 | 69 | public boolean isNotSupportPre(){ 70 | return !mIsSupportPre; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /app/src/main/java/com/utopiaxc/utopiatts/tts/enums/Roles.java: -------------------------------------------------------------------------------- 1 | package com.utopiaxc.utopiatts.tts.enums; 2 | 3 | public enum Roles { 4 | NONE("NONE",""), 5 | YOUNG_ADULT_FEMALE("YOUNG_ADULT_FEMALE","YoungAdultFemale"), 6 | YOUNG_ADULT_MALE("YOUNG_ADULT_MALE","YoungAdultMale"), 7 | OLDER_ADULT_FEMALE("OLDER_ADULT_FEMALE","OlderAdultFemale"), 8 | OLDER_ADULT_MALE("OLDER_ADULT_MALE","OlderAdultMale"), 9 | SENIOR_FEMALE("SENIOR_FEMALE","SeniorFemale"), 10 | SENIOR_MALE("SENIOR_MALE","SeniorMale"), 11 | GIRL("GIRL","Girl"), 12 | BOY("BOY","Boy"), 13 | NARRATOR("NARRATOR","Narrator"); 14 | 15 | private final String mName; 16 | private final String mId; 17 | 18 | Roles(String name, String id){ 19 | mName=name; 20 | mId=id; 21 | } 22 | 23 | public String getName() { 24 | return mName; 25 | } 26 | 27 | public String getId() { 28 | return mId; 29 | } 30 | 31 | public static Roles getRole(String name) { 32 | for (Roles role: Roles.values()) { 33 | if (role.getName().equals(name)) { 34 | return role; 35 | } 36 | } 37 | return NONE; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/utopiaxc/utopiatts/tts/enums/Styles.java: -------------------------------------------------------------------------------- 1 | package com.utopiaxc.utopiatts.tts.enums; 2 | 3 | public enum Styles { 4 | NONE("NONE",""), 5 | CALM("CALM","calm"), 6 | FEARFUL("FEARFUL","fearful"), 7 | CHEERFUL("CHEERFUL","cheerful"), 8 | DISGRUNTLED("DISGRUNTLED","disgruntled"), 9 | SERIOUS("SERIOUS","serious"), 10 | ANGRY("ANGRY","angry"), 11 | GENTLE("GENTLE","gentle"), 12 | AFFECTIONATE("AFFECTIONATE","affectionate"), 13 | EMBARRASSED("EMBARRASSED","embarrassed"), 14 | DEPRESSED("DEPRESSED","depressed"), 15 | ENVIOUS("ENVIOUS","envious"), 16 | ASSISTANT("ASSISTANT","assistant"), 17 | CUSTOMER_SERVICE("CUSTOMER_SERVICE","customerservice"), 18 | NEWSCAST("NEWSCAST","newscast"), 19 | LYRICAL("LYRICAL","lyrical"), 20 | POETRY_READING("POETRY_READING","poetry-reading"), 21 | ADVERTISEMENT_UPBEAT("ADVERTISEMENT_UPBEAT","Advertisement_upbeat"), 22 | SPORTS_COMMENTARY("SPORTS_COMMENTARY","Sports_commentary"), 23 | SPORTS_COMMENTARY_EXCITED("SPORTS_COMMENTARY_EXCITED","Sports_commentary_excited"), 24 | NARRATION_RELAXED("NARRATION_RELAXED","narration-relaxed"), 25 | DOCUMENTARY_NARRATION("DOCUMENTARY_NARRATION","documentary-narration"); 26 | 27 | private final String mName; 28 | private final String mId; 29 | 30 | Styles(String name, String id){ 31 | mName=name; 32 | mId=id; 33 | } 34 | 35 | public String getName() { 36 | return mName; 37 | } 38 | 39 | public String getId() { 40 | return mId; 41 | } 42 | 43 | public static Styles getStyle(String name) { 44 | for (Styles style : Styles.values()) { 45 | if (style.getName().equals(name)) { 46 | return style; 47 | } 48 | } 49 | return NONE; 50 | } 51 | } -------------------------------------------------------------------------------- /app/src/main/java/com/utopiaxc/utopiatts/tts/utils/ByteArrayMediaDataSource.java: -------------------------------------------------------------------------------- 1 | package com.utopiaxc.utopiatts.tts.utils; 2 | 3 | import android.media.MediaDataSource; 4 | import android.os.Build; 5 | 6 | import androidx.annotation.NonNull; 7 | import androidx.annotation.RequiresApi; 8 | 9 | public class ByteArrayMediaDataSource extends MediaDataSource { 10 | private byte[] data; 11 | 12 | public ByteArrayMediaDataSource(@NonNull byte[] data) { 13 | this.data = data; 14 | } 15 | 16 | @Override 17 | public int readAt(long position, byte[] buffer, int offset, int size) { 18 | if (position >= data.length) { 19 | return -1; 20 | } 21 | int endPosition = (int) (position + size); 22 | int size2 = size; 23 | if (endPosition > data.length) { 24 | size2 -= endPosition - data.length; 25 | } 26 | System.arraycopy(data, (int) position, buffer, offset, size2); 27 | return size2; 28 | } 29 | 30 | @Override 31 | public long getSize() { 32 | return data.length; 33 | } 34 | 35 | @Override 36 | public void close() { 37 | data = null; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/utopiaxc/utopiatts/tts/utils/CommonTool.java: -------------------------------------------------------------------------------- 1 | package com.utopiaxc.utopiatts.tts.utils; 2 | 3 | import java.io.PrintWriter; 4 | import java.io.StringWriter; 5 | import java.math.BigDecimal; 6 | import java.math.RoundingMode; 7 | import java.nio.charset.StandardCharsets; 8 | import java.text.SimpleDateFormat; 9 | import java.util.Date; 10 | import java.util.Locale; 11 | import java.util.regex.Pattern; 12 | 13 | import okio.ByteString; 14 | 15 | public class CommonTool { 16 | 17 | static final Pattern NoVoicePattern = Pattern.compile("[\\s\\p{C}\\p{P}\\p{Z}\\p{S}]"); 18 | static final SimpleDateFormat sdf = new SimpleDateFormat( 19 | "EEE MMM dd yyyy HH:mm:ss 'GMT'Z", Locale.ENGLISH); 20 | 21 | 22 | public static boolean isNoVoice(CharSequence charSequence) { 23 | return NoVoicePattern.matcher(charSequence).replaceAll("").isEmpty(); 24 | } 25 | 26 | @SuppressWarnings("unused") 27 | public static void removeAllBlankSpace(StringBuilder sb) { 28 | int j = 0; 29 | for (int i = 0; i < sb.length(); i++) { 30 | if (!(Character.isWhitespace(sb.charAt(i)) || sb.charAt(i) == ' ')) { 31 | sb.setCharAt(j++, sb.charAt(i)); 32 | } 33 | } 34 | sb.delete(j, sb.length()); 35 | } 36 | 37 | public static void Trim(StringBuilder sb) { 38 | if (sb == null || sb.length() == 0) return; 39 | int st = 0; 40 | while (Character.isWhitespace(sb.charAt(st)) || sb.charAt(st) == ' ') { 41 | st++; 42 | } 43 | if (st > 0) { 44 | sb.delete(0, st); 45 | } 46 | 47 | int ed = sb.length(); 48 | while (Character.isWhitespace(sb.charAt(ed - 1)) || sb.charAt(ed - 1) == ' ') { 49 | ed--; 50 | } 51 | if (ed < sb.length()) { 52 | sb.delete(ed, sb.length()); 53 | } 54 | 55 | } 56 | 57 | 58 | public static void replace(StringBuilder builder, String from, String to) { 59 | int index = builder.indexOf(from); 60 | while (index != -1) { 61 | builder.replace(index, index + from.length(), to); 62 | index += to.length(); // Move to the end of the replacement 63 | index = builder.indexOf(from, index); 64 | } 65 | } 66 | 67 | public static String getStackTrace(Throwable throwable) { 68 | StringWriter sw = new StringWriter(); 69 | 70 | try (PrintWriter pw = new PrintWriter(sw)) { 71 | throwable.printStackTrace(pw); 72 | return sw.toString(); 73 | } 74 | } 75 | 76 | public static String getTime() { 77 | Date date = new Date(); 78 | return sdf.format(date); 79 | } 80 | 81 | public static String getTime(long timestamp) { 82 | Date date = new Date(timestamp); 83 | return sdf.format(date); 84 | } 85 | 86 | public static String getMD5String(String str) { 87 | return ByteString.of(str.getBytes(StandardCharsets.UTF_8)).md5().hex(); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /app/src/main/java/com/utopiaxc/utopiatts/tts/utils/Constants.java: -------------------------------------------------------------------------------- 1 | package com.utopiaxc.utopiatts.tts.utils; 2 | 3 | public final class Constants { 4 | public static final String EDGE_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + 5 | "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36 " + 6 | "Edg/106.0.1370.42"; 7 | public static final String WSS = "wss://"; 8 | 9 | public static final String MS_API = ".api.speech.microsoft.com/" + 10 | "cognitiveservices/websocket/v1?TrafficType=AzureDemo&Authorization=bearer " + 11 | "undefined&X-ConnectionId="; 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/utopiaxc/utopiatts/tts/utils/Ssml.java: -------------------------------------------------------------------------------- 1 | package com.utopiaxc.utopiatts.tts.utils; 2 | 3 | import android.util.Log; 4 | 5 | import androidx.annotation.NonNull; 6 | 7 | import java.util.List; 8 | 9 | public class Ssml { 10 | private static final String TAG = "Ssml"; 11 | private final String mActor; 12 | private final int mPitch; 13 | private final int mRate; 14 | private final String mStyle; 15 | private final int mStyleDegree; 16 | private final StringBuilder mText; 17 | private final String mRole; 18 | 19 | public Ssml(String text, String actor, int pitch, int rate, String role, String style, 20 | int styleDegree) { 21 | mText = new StringBuilder(text); 22 | mActor = actor; 23 | mPitch = (pitch / 4) - 25; 24 | mRate = (rate / 4) - 25; 25 | mRole = role; 26 | mStyle = style; 27 | mStyleDegree = styleDegree; 28 | handleContent(); 29 | } 30 | 31 | 32 | @NonNull 33 | @Override 34 | public String toString() { 35 | StringBuilder sb = new StringBuilder(); 36 | sb.append(""); 40 | sb.append(""); 41 | if (!"".equals(mStyle) || !"".equals(mRole)) { 42 | sb.append(""); 51 | } 52 | sb.append(""); 54 | sb.append(mText); 55 | sb.append(""); 56 | if (!"".equals(mStyle)) { 57 | sb.append(""); 58 | } 59 | sb.append(""); 60 | Log.d(TAG, "toString = " + sb); 61 | return sb.toString(); 62 | } 63 | 64 | public String toStringForWs() { 65 | long timestamp = System.currentTimeMillis(); 66 | StringBuilder sb = new StringBuilder() 67 | .append("Path:ssml\r\n") 68 | .append("X-RequestId:").append(CommonTool.getMD5String(mText + "" + timestamp)) 69 | .append("\r\n") 70 | .append("X-Timestamp:") 71 | .append(CommonTool.getTime(timestamp)).append("Z\r\n") 72 | .append("Content-Type:application/ssml+xml\r\n\r\n"); 73 | sb.append(""); 77 | sb.append(""); 78 | if (!"".equals(mStyle) || !"".equals(mRole)) { 79 | sb.append(""); 88 | } 89 | sb.append(""); 91 | sb.append(mText); 92 | sb.append(""); 93 | if (!"".equals(mStyle)) { 94 | sb.append(""); 95 | } 96 | sb.append(""); 97 | Log.d(TAG, "toString = " + sb); 98 | return sb.toString(); 99 | } 100 | 101 | private void handleContent() { 102 | CommonTool.replace(mText, "\n", " "); 103 | CommonTool.Trim(mText); 104 | CommonTool.replace(mText, "&", "&"); 105 | CommonTool.replace(mText, "\"", """); 106 | CommonTool.replace(mText, "'", "'"); 107 | CommonTool.replace(mText, ">", "<"); 108 | CommonTool.replace(mText, "<", ">"); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /app/src/main/java/com/utopiaxc/utopiatts/tts/utils/WebSocketState.java: -------------------------------------------------------------------------------- 1 | package com.utopiaxc.utopiatts.tts.utils; 2 | 3 | public enum WebSocketState { 4 | OFFLINE,//断线 5 | CONNECTED,//已连接 6 | CONNECTING //连接中 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/utopiaxc/utopiatts/usage/FragmentUsageDirect.java: -------------------------------------------------------------------------------- 1 | package com.utopiaxc.utopiatts.usage; 2 | 3 | import androidx.fragment.app.Fragment; 4 | 5 | public class FragmentUsageDirect extends Fragment { 6 | 7 | 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/utopiaxc/utopiatts/usage/FragmentUsageInnerApp.java: -------------------------------------------------------------------------------- 1 | package com.utopiaxc.utopiatts.usage; 2 | 3 | import androidx.fragment.app.Fragment; 4 | 5 | public class FragmentUsageInnerApp extends Fragment { 6 | } 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/utopiaxc/utopiatts/usage/FragmentUsageSystem.java: -------------------------------------------------------------------------------- 1 | package com.utopiaxc.utopiatts.usage; 2 | 3 | import androidx.fragment.app.Fragment; 4 | 5 | public class FragmentUsageSystem extends Fragment { 6 | } 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/utopiaxc/utopiatts/utils/ThemeUtil.java: -------------------------------------------------------------------------------- 1 | package com.utopiaxc.utopiatts.utils; 2 | 3 | import android.content.Context; 4 | import android.content.res.Configuration; 5 | 6 | import androidx.appcompat.app.AppCompatDelegate; 7 | 8 | import com.utopiaxc.utopiatts.enums.ThemeModeEnum; 9 | 10 | public class ThemeUtil { 11 | public static void setThemeMode(String mode) { 12 | if (mode.equals(ThemeModeEnum.AUTO_MODE.getMode())) { 13 | AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); 14 | } else if (mode.equals(ThemeModeEnum.NIGHT_MODE.getMode())) { 15 | AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES); 16 | } else if (mode.equals(ThemeModeEnum.DAY_MODE.getMode())) { 17 | AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO); 18 | } 19 | } 20 | 21 | public static boolean isNightMode(Context context){ 22 | int mode = context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; 23 | return mode == Configuration.UI_MODE_NIGHT_YES; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/utopiaxc/utopiatts/welcome/FragmentSetAzureToken.kt: -------------------------------------------------------------------------------- 1 | package com.utopiaxc.utopiatts.welcome 2 | 3 | import android.app.AlertDialog 4 | import android.content.Context 5 | import android.content.res.Resources.Theme 6 | import android.os.Bundle 7 | import android.os.Handler 8 | import android.os.Looper 9 | import android.os.Message 10 | import android.util.Log 11 | import android.view.LayoutInflater 12 | import android.view.View 13 | import android.view.ViewGroup 14 | import android.view.inputmethod.InputMethodManager 15 | import androidx.fragment.app.Fragment 16 | import androidx.preference.PreferenceManager 17 | import com.github.appintro.SlidePolicy 18 | import com.utopiaxc.utopiatts.R 19 | import com.utopiaxc.utopiatts.databinding.FragmentSetAzureTokenBinding 20 | import com.utopiaxc.utopiatts.enums.SettingsEnum 21 | import com.utopiaxc.utopiatts.tts.MsTts 22 | import com.utopiaxc.utopiatts.tts.enums.Regions 23 | import com.utopiaxc.utopiatts.utils.ThemeUtil 24 | 25 | 26 | class FragmentSetAzureToken(private var mContext: IntroActivity) : Fragment(), SlidePolicy { 27 | private val TAG = "FragmentSetAzureToken" 28 | private lateinit var mBinding: FragmentSetAzureTokenBinding 29 | private var mIsChecked = false 30 | private lateinit var mFragmentSetAzureTokenHandler: FragmentSetAzureTokenHandler 31 | 32 | override fun onCreate(savedInstanceState: Bundle?) { 33 | super.onCreate(savedInstanceState) 34 | mBinding = FragmentSetAzureTokenBinding.inflate(layoutInflater) 35 | mFragmentSetAzureTokenHandler = FragmentSetAzureTokenHandler(mContext.mainLooper) 36 | } 37 | 38 | override fun onCreateView( 39 | inflater: LayoutInflater, container: ViewGroup?, 40 | savedInstanceState: Bundle? 41 | ): View { 42 | Log.d(TAG, "onCreateView") 43 | if (ThemeUtil.isNightMode(mContext)) { 44 | mBinding.root.setBackgroundColor(mContext.getColor(R.color.welcome_bg_night)) 45 | } else { 46 | mBinding.root.setBackgroundColor(mContext.getColor(R.color.welcome_bg_day)) 47 | } 48 | 49 | mBinding.buttonSetAzureToken.setOnClickListener { 50 | val imm: InputMethodManager = requireView().context 51 | .getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager 52 | imm.hideSoftInputFromWindow(requireView().windowToken, 0) 53 | val token = mBinding.editTextAzureToken.text.toString() 54 | if (token == "") { 55 | AlertDialog.Builder(this.activity).setTitle(R.string.warning) 56 | .setMessage(R.string.warning_blank_token) 57 | .setPositiveButton(R.string.confirm, null) 58 | .create() 59 | .show() 60 | return@setOnClickListener 61 | } 62 | val region = Regions.getRegion( 63 | resources.getStringArray(R.array.azure_region_values) 64 | [mBinding.spinnerAzureRegion.selectedItemId.toInt()] 65 | ).id 66 | mBinding.buttonSetAzureToken.isEnabled = false 67 | mBinding.buttonSetAzureToken.text = getString(R.string.checking) 68 | Thread(CheckToken(token, region)).start() 69 | } 70 | mBinding.buttonAzureTokenHelp.setOnClickListener { 71 | AlertDialog.Builder(this.activity).setTitle(R.string.tips) 72 | .setMessage(R.string.tips_azure_token) 73 | .setPositiveButton(R.string.confirm, null) 74 | .setNegativeButton(R.string.tutorials) { _, _ -> 75 | run { 76 | AlertDialog.Builder(this.activity).setTitle(R.string.sorry) 77 | .setMessage(R.string.tutorials_is_not_ready) 78 | .setPositiveButton(R.string.confirm, null) 79 | .create() 80 | .show() 81 | } 82 | } 83 | .create() 84 | .show() 85 | } 86 | return mBinding.root 87 | } 88 | 89 | companion object { 90 | @JvmStatic 91 | fun newInstance(context: IntroActivity): FragmentSetAzureToken { 92 | return FragmentSetAzureToken(context) 93 | } 94 | } 95 | 96 | override val isPolicyRespected: Boolean 97 | get() = mIsChecked 98 | 99 | override fun onUserIllegallyRequestedNextPage() { 100 | AlertDialog.Builder(this.activity).setTitle(R.string.warning) 101 | .setMessage(R.string.warning_azure_token_not_checked) 102 | .setPositiveButton(R.string.confirm, null) 103 | .create() 104 | .show() 105 | } 106 | 107 | private inner class CheckToken(var token: String, var region: String) : Runnable { 108 | 109 | override fun run() { 110 | if (MsTts.testAzureConfig(token, region)) { 111 | val message = Message() 112 | message.what = mFragmentSetAzureTokenHandler.CHECK_TOKEN_SUCCESS 113 | val bundle = Bundle() 114 | bundle.putString(SettingsEnum.AZURE_TOKEN.key, token) 115 | bundle.putString(SettingsEnum.AZURE_REGION.key, Regions.getRegion(region).getName()) 116 | message.data = bundle 117 | mFragmentSetAzureTokenHandler.sendMessage(message) 118 | } else { 119 | mFragmentSetAzureTokenHandler.sendEmptyMessage( 120 | mFragmentSetAzureTokenHandler.CHECK_TOKEN_ERROR 121 | ) 122 | } 123 | } 124 | } 125 | 126 | private inner class FragmentSetAzureTokenHandler(looper: Looper) : Handler(looper) { 127 | val CHECK_TOKEN_SUCCESS = 0x01 128 | val CHECK_TOKEN_ERROR = 0x02 129 | 130 | override fun handleMessage(msg: Message) { 131 | super.handleMessage(msg) 132 | when (msg.what) { 133 | CHECK_TOKEN_SUCCESS -> { 134 | val bundle = msg.data 135 | mBinding.buttonSetAzureToken.isEnabled = true 136 | mBinding.buttonSetAzureToken.text = getString(R.string.success) 137 | mBinding.buttonSetAzureToken 138 | .setBackgroundColor(resources.getColor(R.color.success)) 139 | val editor = PreferenceManager.getDefaultSharedPreferences(mContext).edit() 140 | editor.putString( 141 | SettingsEnum.AZURE_TOKEN.key, bundle 142 | .getString(SettingsEnum.AZURE_TOKEN.key) 143 | ) 144 | editor.putString( 145 | SettingsEnum.AZURE_REGION.key, bundle 146 | .getString(SettingsEnum.AZURE_REGION.key) 147 | ) 148 | editor.apply() 149 | mIsChecked = true 150 | } 151 | CHECK_TOKEN_ERROR -> { 152 | mBinding.buttonSetAzureToken.isEnabled = true 153 | mBinding.buttonSetAzureToken 154 | .setBackgroundColor(resources.getColor(R.color.warning)) 155 | mBinding.buttonSetAzureToken.text = getString(R.string.error) 156 | AlertDialog.Builder(mContext).setTitle(R.string.error) 157 | .setMessage(R.string.error_azure_token) 158 | .setPositiveButton(R.string.confirm, null) 159 | .create() 160 | .show() 161 | } 162 | } 163 | } 164 | } 165 | } -------------------------------------------------------------------------------- /app/src/main/java/com/utopiaxc/utopiatts/welcome/FragmentTtsSelect.kt: -------------------------------------------------------------------------------- 1 | package com.utopiaxc.utopiatts.welcome 2 | 3 | import android.app.AlertDialog 4 | import android.content.Intent 5 | import android.net.Uri 6 | import android.os.Bundle 7 | import android.util.Log 8 | import android.view.LayoutInflater 9 | import android.view.View 10 | import android.view.ViewGroup 11 | import androidx.fragment.app.Fragment 12 | import androidx.preference.PreferenceManager 13 | import com.github.appintro.SlidePolicy 14 | import com.utopiaxc.utopiatts.MainActivity 15 | import com.utopiaxc.utopiatts.R 16 | import com.utopiaxc.utopiatts.databinding.FragmentTtsSelectBinding 17 | import com.utopiaxc.utopiatts.enums.SettingsEnum 18 | import com.utopiaxc.utopiatts.tts.enums.Driver 19 | import com.utopiaxc.utopiatts.utils.ThemeUtil 20 | 21 | 22 | class FragmentTtsSelect(private var mContext: IntroActivity) : Fragment(), SlidePolicy { 23 | private val TAG = "FragmentTtsSelect" 24 | private var mIsChecked = true 25 | private lateinit var mBinding: FragmentTtsSelectBinding 26 | 27 | override fun onCreate(savedInstanceState: Bundle?) { 28 | super.onCreate(savedInstanceState) 29 | mBinding = FragmentTtsSelectBinding.inflate(layoutInflater) 30 | 31 | } 32 | 33 | override fun onCreateView( 34 | inflater: LayoutInflater, container: ViewGroup?, 35 | savedInstanceState: Bundle? 36 | ): View { 37 | val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext) 38 | val editor = sharedPreferences.edit() 39 | editor.putString( 40 | SettingsEnum.TTS_DRIVER.key, 41 | SettingsEnum.TTS_DRIVER.defaultValue as String 42 | ) 43 | editor.apply() 44 | if (ThemeUtil.isNightMode(mContext)) { 45 | mBinding.root.setBackgroundColor(mContext.getColor(R.color.welcome_bg_night)) 46 | mBinding.buttonTtsFromSelectHelp.setImageResource(R.drawable.ic_baseline_help_outline_24_night) 47 | } else { 48 | mBinding.root.setBackgroundColor(mContext.getColor(R.color.welcome_bg_day)) 49 | } 50 | mBinding.buttonTtsFromSelectHelp.setOnClickListener { 51 | AlertDialog.Builder(this.activity).setTitle(R.string.tips) 52 | .setMessage(R.string.tips_tts_driver) 53 | .setPositiveButton(R.string.confirm, null) 54 | .setNeutralButton(R.string.ag2s20150909_tts_title) { _, _ -> 55 | run { 56 | val uri: Uri = Uri.parse(getString(R.string.ag2s20150909_tts)) 57 | val intent = Intent(Intent.ACTION_VIEW, uri) 58 | startActivity(intent) 59 | } 60 | } 61 | .create() 62 | .show() 63 | } 64 | mBinding.ttsFromSdk.setOnClickListener { 65 | Log.d(TAG, "ttsFromSdk clicked") 66 | editor.putString(SettingsEnum.TTS_DRIVER.key, Driver.AZURE_SDK.id) 67 | editor.apply() 68 | mIsChecked=true 69 | } 70 | mBinding.ttsFromWebsocket.setOnClickListener { 71 | Log.d(TAG, "ttsFromWebsocket clicked") 72 | editor.putString(SettingsEnum.TTS_DRIVER.key, Driver.WEBSOCKET.id) 73 | editor.apply() 74 | mIsChecked=false 75 | } 76 | return mBinding.root 77 | } 78 | 79 | override val isPolicyRespected: Boolean 80 | get() = mIsChecked 81 | 82 | override fun onUserIllegallyRequestedNextPage() { 83 | val editor = PreferenceManager.getDefaultSharedPreferences(mContext).edit() 84 | editor.putBoolean(SettingsEnum.FIRST_BOOT.key, false) 85 | editor.apply() 86 | mContext.startActivity(Intent(mContext, MainActivity::class.java)) 87 | mContext.finish() 88 | } 89 | 90 | companion object { 91 | @JvmStatic 92 | fun newInstance(context: IntroActivity): FragmentTtsSelect { 93 | return FragmentTtsSelect(context) 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /app/src/main/java/com/utopiaxc/utopiatts/welcome/IntroActivity.kt: -------------------------------------------------------------------------------- 1 | package com.utopiaxc.utopiatts.welcome 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import androidx.fragment.app.Fragment 6 | import androidx.preference.PreferenceManager 7 | import com.github.appintro.AppIntro2 8 | import com.github.appintro.AppIntroFragment 9 | import com.utopiaxc.utopiatts.MainActivity 10 | import com.utopiaxc.utopiatts.R 11 | import com.utopiaxc.utopiatts.enums.SettingsEnum 12 | import com.utopiaxc.utopiatts.utils.ThemeUtil 13 | 14 | class IntroActivity : AppIntro2() { 15 | override fun onCreate(savedInstanceState: Bundle?) { 16 | super.onCreate(savedInstanceState) 17 | isWizardMode = true 18 | supportActionBar?.hide() 19 | setSwipeLock(true) 20 | 21 | val stringColor: Int 22 | val bgColor: Int 23 | if (ThemeUtil.isNightMode(this)) { 24 | stringColor = R.color.welcome_string_night 25 | bgColor = R.color.welcome_bg_night 26 | } else { 27 | stringColor = R.color.welcome_string_day 28 | bgColor = R.color.welcome_bg_day 29 | } 30 | 31 | //First Fragment 32 | addSlide( 33 | AppIntroFragment.createInstance( 34 | getString(R.string.welcome), 35 | getString(R.string.rights), 36 | R.mipmap.ic_launcher, 37 | bgColor, 38 | stringColor, 39 | stringColor, 40 | ) 41 | ) 42 | 43 | addSlide(FragmentTtsSelect.newInstance(this)) 44 | 45 | addSlide(FragmentSetAzureToken.newInstance(this)) 46 | 47 | } 48 | 49 | override fun onDonePressed(currentFragment: Fragment?) { 50 | super.onDonePressed(currentFragment) 51 | val editor = PreferenceManager.getDefaultSharedPreferences(this).edit() 52 | editor.putBoolean(SettingsEnum.FIRST_BOOT.key, false) 53 | editor.apply() 54 | startActivity(Intent(this, MainActivity::class.java)) 55 | finish() 56 | } 57 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UtopiaXC/UtopiaTTS/b609bd98ba9244d11c3e8db13fc0380d3051766a/app/src/main/res/drawable-v24/logo.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_code_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_help_outline_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_help_outline_24_night.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_settings_accessibility_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_sharp_hearing_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 21 | 22 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_set_azure_token.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 20 | 21 |