diff --git a/android/README.md b/android/README.md
index 16489dbea5..053ad66e00 100644
--- a/android/README.md
+++ b/android/README.md
@@ -13,3 +13,8 @@ for usage.
- [SherpaOnnxVadAsr](./SherpaOnnxVadAsr) It uses a VAD with a non-streaming
ASR model.
+
+- [SherpaOnnxTts](./SherpaOnnxTts) It is for standalone text-to-speech.
+
+- [SherpaOnnxTtsEngine](./SherpaOnnxTtsEngine) It is for text-to-speech engine;
+ you can use it to replace the system TTS engine.
diff --git a/android/SherpaOnnxTts/app/src/main/java/com/k2fsa/sherpa/onnx/Tts.kt b/android/SherpaOnnxTts/app/src/main/java/com/k2fsa/sherpa/onnx/Tts.kt
index bb50044019..be48b6db8e 100644
--- a/android/SherpaOnnxTts/app/src/main/java/com/k2fsa/sherpa/onnx/Tts.kt
+++ b/android/SherpaOnnxTts/app/src/main/java/com/k2fsa/sherpa/onnx/Tts.kt
@@ -56,6 +56,8 @@ class OfflineTts(
fun sampleRate() = getSampleRate(ptr)
+ fun numSpeakers() = getNumSpeakers(ptr)
+
fun generate(
text: String,
sid: Int = 0,
@@ -113,6 +115,7 @@ class OfflineTts(
private external fun delete(ptr: Long)
private external fun getSampleRate(ptr: Long): Int
+ private external fun getNumSpeakers(ptr: Long): Int
// The returned array has two entries:
// - the first entry is an 1-D float array containing audio samples.
diff --git a/android/SherpaOnnxTtsEngine/.gitignore b/android/SherpaOnnxTtsEngine/.gitignore
new file mode 100644
index 0000000000..aa724b7707
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine/.gitignore
@@ -0,0 +1,15 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
diff --git a/android/SherpaOnnxTtsEngine/app/.gitignore b/android/SherpaOnnxTtsEngine/app/.gitignore
new file mode 100644
index 0000000000..42afabfd2a
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine/app/build.gradle.kts b/android/SherpaOnnxTtsEngine/app/build.gradle.kts
new file mode 100644
index 0000000000..dd62850c98
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine/app/build.gradle.kts
@@ -0,0 +1,71 @@
+plugins {
+ id("com.android.application")
+ id("org.jetbrains.kotlin.android")
+}
+
+android {
+ namespace = "com.k2fsa.sherpa.onnx.tts.engine"
+ compileSdk = 34
+
+ defaultConfig {
+ applicationId = "com.k2fsa.sherpa.onnx.tts.engine"
+ minSdk = 21
+ targetSdk = 34
+ versionCode = 1
+ versionName = "1.0"
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ vectorDrawables {
+ useSupportLibrary = true
+ }
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+ buildFeatures {
+ compose = true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion = "1.5.1"
+ }
+ packaging {
+ resources {
+ excludes += "/META-INF/{AL2.0,LGPL2.1}"
+ }
+ }
+}
+
+dependencies {
+
+ implementation("androidx.core:core-ktx:1.12.0")
+ implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
+ implementation("androidx.activity:activity-compose:1.8.2")
+ implementation(platform("androidx.compose:compose-bom:2023.08.00"))
+ implementation("androidx.compose.ui:ui")
+ implementation("androidx.compose.ui:ui-graphics")
+ implementation("androidx.compose.ui:ui-tooling-preview")
+ implementation("androidx.compose.material3:material3")
+ implementation("androidx.appcompat:appcompat:1.6.1")
+ implementation("com.google.android.material:material:1.9.0")
+ testImplementation("junit:junit:4.13.2")
+ androidTestImplementation("androidx.test.ext:junit:1.1.5")
+ androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
+ androidTestImplementation(platform("androidx.compose:compose-bom:2023.08.00"))
+ androidTestImplementation("androidx.compose.ui:ui-test-junit4")
+ debugImplementation("androidx.compose.ui:ui-tooling")
+ debugImplementation("androidx.compose.ui:ui-test-manifest")
+}
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine/app/proguard-rules.pro b/android/SherpaOnnxTtsEngine/app/proguard-rules.pro
new file mode 100644
index 0000000000..481bb43481
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine/app/src/androidTest/java/com/k2fsa/sherpa/onnx/tts/engine/ExampleInstrumentedTest.kt b/android/SherpaOnnxTtsEngine/app/src/androidTest/java/com/k2fsa/sherpa/onnx/tts/engine/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000000..6713b5be7c
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine/app/src/androidTest/java/com/k2fsa/sherpa/onnx/tts/engine/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.k2fsa.sherpa.onnx.tts.engine
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("com.k2fsa.sherpa.onnx.tts.engine", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine/app/src/main/AndroidManifest.xml b/android/SherpaOnnxTtsEngine/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..64d7faafe1
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine/app/src/main/AndroidManifest.xml
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine/app/src/main/assets/.gitkeep b/android/SherpaOnnxTtsEngine/app/src/main/assets/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/android/SherpaOnnxTtsEngine/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/CheckVoiceData.kt b/android/SherpaOnnxTtsEngine/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/CheckVoiceData.kt
new file mode 100644
index 0000000000..9ddc138209
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/CheckVoiceData.kt
@@ -0,0 +1,18 @@
+package com.k2fsa.sherpa.onnx.tts.engine
+
+import android.content.Intent
+import androidx.appcompat.app.AppCompatActivity
+import android.os.Bundle
+import android.speech.tts.TextToSpeech
+
+class CheckVoiceData : AppCompatActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val intent = Intent().apply {
+ putStringArrayListExtra(TextToSpeech.Engine.EXTRA_AVAILABLE_VOICES, arrayListOf(TtsEngine.lang))
+ putStringArrayListExtra(TextToSpeech.Engine.EXTRA_UNAVAILABLE_VOICES, arrayListOf())
+ }
+ setResult(TextToSpeech.Engine.CHECK_VOICE_DATA_PASS, intent)
+ finish()
+ }
+}
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/GetSampleText.kt b/android/SherpaOnnxTtsEngine/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/GetSampleText.kt
new file mode 100644
index 0000000000..fe788afe02
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/GetSampleText.kt
@@ -0,0 +1,37 @@
+package com.k2fsa.sherpa.onnx.tts.engine
+
+import android.app.Activity
+import android.content.Intent
+import androidx.appcompat.app.AppCompatActivity
+import android.os.Bundle
+import android.speech.tts.TextToSpeech
+
+class GetSampleText : Activity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ var result = TextToSpeech.LANG_AVAILABLE
+ var text: String = ""
+ when(TtsEngine.lang) {
+ "eng" -> {
+ text = "This is a text-to-speech engine with next generation Kaldi"
+ }
+ "zho", "cmn" -> {
+ text = "使用新一代 Kaldi 进行语音合成"
+ }
+ else -> {
+ result = TextToSpeech.LANG_NOT_SUPPORTED
+ }
+ }
+
+ val intent = Intent().apply{
+ if(result == TextToSpeech.LANG_AVAILABLE) {
+ putExtra(TextToSpeech.Engine.EXTRA_SAMPLE_TEXT, text)
+ } else {
+ putExtra("sampleText", text)
+ }
+ }
+
+ setResult(result, intent)
+ finish()
+ }
+}
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/InstallVoiceData.kt b/android/SherpaOnnxTtsEngine/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/InstallVoiceData.kt
new file mode 100644
index 0000000000..6b30546a05
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/InstallVoiceData.kt
@@ -0,0 +1,12 @@
+package com.k2fsa.sherpa.onnx.tts.engine
+
+import android.app.Activity
+import android.os.Bundle
+import android.view.Window
+
+class InstallVoiceData : Activity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ requestWindowFeature(Window.FEATURE_NO_TITLE)
+ super.onCreate(savedInstanceState)
+ }
+}
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/MainActivity.kt b/android/SherpaOnnxTtsEngine/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/MainActivity.kt
new file mode 100644
index 0000000000..b38e00a05d
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/MainActivity.kt
@@ -0,0 +1,141 @@
+@file:OptIn(ExperimentalMaterial3Api::class)
+
+package com.k2fsa.sherpa.onnx.tts.engine
+
+import android.media.MediaPlayer
+import android.net.Uri
+import android.os.Bundle
+import android.util.Log
+import android.widget.Toast
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.material3.Button
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Slider
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextField
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.k2fsa.sherpa.onnx.tts.engine.ui.theme.SherpaOnnxTtsEngineTheme
+import java.io.File
+
+const val TAG = "sherpa-onnx-tts-engine"
+
+class MainActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ TtsEngine.createTts(this.application)
+ setContent {
+ SherpaOnnxTtsEngineTheme {
+ // A surface container using the 'background' color from the theme
+ Surface(
+ modifier = Modifier.fillMaxSize(),
+ color = MaterialTheme.colorScheme.background
+ ) {
+ Scaffold(topBar = {
+ TopAppBar(title = { Text("Next-gen Kaldi: TTS") })
+ }) {
+ Box(modifier = Modifier.padding(it)) {
+ Column {
+ Row {
+ Text("Speed")
+ Slider(
+ value = TtsEngine.speedState.value,
+ onValueChange = { TtsEngine.speed = it },
+ valueRange = 0.2F..3.0F
+ )
+ }
+ var testText by remember { mutableStateOf("") }
+
+ OutlinedTextField(value = testText,
+ onValueChange = { testText = it },
+ label = { Text ("Test text") },
+ modifier = Modifier.fillMaxWidth().wrapContentHeight().padding(16.dp),
+ singleLine = false,
+ )
+
+ val numSpeakers = TtsEngine.tts!!.numSpeakers()
+ if (numSpeakers > 1) {
+ Row {
+ Text("Speaker ID: (0-${numSpeakers - 1})")
+ Slider(
+ value = TtsEngine.speakerIdState.value.toFloat(),
+ onValueChange = { TtsEngine.speakerId = it.toInt() },
+ valueRange = 0.0f..(numSpeakers - 1).toFloat(),
+ steps = 1
+ )
+ }
+ }
+ Row {
+ Button(
+ modifier = Modifier.padding(20.dp),
+ onClick = {
+ Log.i(TAG, "Clicked, text: ${testText}")
+ if (testText.isBlank() || testText.isEmpty()) {
+ Toast.makeText(
+ applicationContext,
+ "Please input a test sentence",
+ Toast.LENGTH_SHORT
+ ).show()
+ } else {
+ val audio = TtsEngine.tts!!.generate(
+ text = testText,
+ sid = TtsEngine.speakerId,
+ speed = TtsEngine.speed,
+ )
+
+ val filename =
+ application.filesDir.absolutePath + "/generated.wav"
+ val ok =
+ audio.samples.size > 0 && audio.save(filename)
+
+ if (ok) {
+ val mediaPlayer = MediaPlayer.create(
+ applicationContext,
+ Uri.fromFile(File(filename))
+ )
+ mediaPlayer.start()
+ } else {
+ Log.i(TAG, "Failed to generate or save audio")
+ }
+ }
+ }) {
+ Text("Test")
+ }
+
+ Button(
+ modifier = Modifier.padding(20.dp),
+ onClick = {
+ TtsEngine.speakerId = 0
+ TtsEngine.speed = 1.0f
+ testText = ""
+ }) {
+ Text("Reset")
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/Tts.kt b/android/SherpaOnnxTtsEngine/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/Tts.kt
new file mode 120000
index 0000000000..bc6a22c571
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/Tts.kt
@@ -0,0 +1 @@
+../../../../../../../../../../../SherpaOnnxTts/app/src/main/java/com/k2fsa/sherpa/onnx/Tts.kt
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/TtsEngine.kt b/android/SherpaOnnxTtsEngine/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/TtsEngine.kt
new file mode 100644
index 0000000000..4fc935b1b2
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/TtsEngine.kt
@@ -0,0 +1,158 @@
+package com.k2fsa.sherpa.onnx.tts.engine
+
+import android.app.Application
+import android.content.res.AssetManager
+import android.util.Log
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import com.k2fsa.sherpa.onnx.*
+import java.io.File
+import java.io.FileOutputStream
+import java.io.IOException
+
+object TtsEngine {
+ var tts: OfflineTts? = null
+
+ // https://en.wikipedia.org/wiki/ISO_639-3
+ // Example:
+ // eng for English,
+ // deu for German
+ // cmn for Mandarin
+ var lang: String? = null
+
+
+
+ val speedState: MutableState = mutableStateOf(1.0F)
+ val speakerIdState: MutableState = mutableStateOf(0)
+
+ var speed: Float
+ get() = speedState.value
+ set(value) {
+ speedState.value = value
+ }
+
+ var speakerId: Int
+ get() = speakerIdState.value
+ set(value) {
+ speakerIdState.value = value
+ }
+
+ private var modelDir: String? = null
+ private var modelName: String? = null
+ private var ruleFsts: String? = null
+ private var lexicon: String? = null
+ private var dataDir: String? = null
+ private var assets: AssetManager? = null
+
+ private var application: Application? = null
+
+ fun createTts(application: Application) {
+ Log.i(TAG, "Init Next-gen Kaldi TTS")
+ if (tts == null) {
+ this.application = application
+ initTts()
+ }
+ }
+
+ private fun initTts() {
+ assets = application?.assets
+
+ // The purpose of such a design is to make the CI test easier
+ // Please see
+ // https://github.com/k2-fsa/sherpa-onnx/blob/master/scripts/apk/generate-tts-apk-script.py
+ modelDir = null
+ modelName = null
+ ruleFsts = null
+ lexicon = null
+ dataDir = null
+ lang = null
+
+ // Please enable one and only one of the examples below
+
+ // Example 1:
+ // modelDir = "vits-vctk"
+ // modelName = "vits-vctk.onnx"
+ // lexicon = "lexicon.txt"
+ // lang = "eng"
+
+ // Example 2:
+ // https://github.com/k2-fsa/sherpa-onnx/releases/tag/tts-models
+ // https://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models/vits-piper-en_US-amy-low.tar.bz2
+ // modelDir = "vits-piper-en_US-amy-low"
+ // modelName = "en_US-amy-low.onnx"
+ // dataDir = "vits-piper-en_US-amy-low/espeak-ng-data"
+ // lang = "eng"
+
+ // Example 3:
+ // modelDir = "vits-zh-aishell3"
+ // modelName = "vits-aishell3.onnx"
+ // ruleFsts = "vits-zh-aishell3/rule.fst"
+ // lexcion = "lexicon.txt"
+ // lang = "zho"
+
+ if (dataDir != null) {
+ val newDir = copyDataDir(modelDir!!)
+ modelDir = newDir + "/" + modelDir
+ dataDir = newDir + "/" + dataDir
+ assets = null
+ }
+
+ val config = getOfflineTtsConfig(
+ modelDir = modelDir!!, modelName = modelName!!, lexicon = lexicon ?: "",
+ dataDir = dataDir ?: "",
+ ruleFsts = ruleFsts ?: ""
+ )!!
+
+ tts = OfflineTts(assetManager = assets, config = config)
+ }
+
+
+ private fun copyDataDir(dataDir: String): String {
+ println("data dir is $dataDir")
+ copyAssets(dataDir)
+
+ val newDataDir = application!!.getExternalFilesDir(null)!!.absolutePath
+ println("newDataDir: $newDataDir")
+ return newDataDir
+ }
+
+ private fun copyAssets(path: String) {
+ val assets: Array?
+ try {
+ assets = application!!.assets.list(path)
+ if (assets!!.isEmpty()) {
+ copyFile(path)
+ } else {
+ val fullPath = "${application!!.getExternalFilesDir(null)}/$path"
+ val dir = File(fullPath)
+ dir.mkdirs()
+ for (asset in assets.iterator()) {
+ val p: String = if (path == "") "" else path + "/"
+ copyAssets(p + asset)
+ }
+ }
+ } catch (ex: IOException) {
+ Log.e(TAG, "Failed to copy $path. ${ex.toString()}")
+ }
+ }
+
+ private fun copyFile(filename: String) {
+ try {
+ val istream = application!!.assets.open(filename)
+ val newFilename = application!!.getExternalFilesDir(null).toString() + "/" + filename
+ val ostream = FileOutputStream(newFilename)
+ // Log.i(TAG, "Copying $filename to $newFilename")
+ val buffer = ByteArray(1024)
+ var read = 0
+ while (read != -1) {
+ ostream.write(buffer, 0, read)
+ read = istream.read(buffer)
+ }
+ istream.close()
+ ostream.flush()
+ ostream.close()
+ } catch (ex: Exception) {
+ Log.e(TAG, "Failed to copy $filename, ${ex.toString()}")
+ }
+ }
+}
diff --git a/android/SherpaOnnxTtsEngine/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/TtsService.kt b/android/SherpaOnnxTtsEngine/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/TtsService.kt
new file mode 100644
index 0000000000..99e043503c
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/TtsService.kt
@@ -0,0 +1,155 @@
+package com.k2fsa.sherpa.onnx.tts.engine
+
+import android.media.AudioFormat
+import android.speech.tts.SynthesisCallback
+import android.speech.tts.SynthesisRequest
+import android.speech.tts.TextToSpeech
+import android.speech.tts.TextToSpeechService
+import android.util.Log
+import com.k2fsa.sherpa.onnx.*
+
+/*
+https://developer.android.com/reference/java/util/Locale#getISO3Language()
+https://developer.android.com/reference/java/util/Locale#getISO3Country()
+
+eng, USA,
+eng, USA, POSIX
+eng,
+eng, GBR
+afr,
+afr, NAM
+afr, ZAF
+agq
+agq, CMR
+aka,
+aka, GHA
+amh,
+amh, ETH
+ara,
+ara, 001
+ara, ARE
+ara, BHR,
+deu
+deu, AUT
+deu, BEL
+deu, CHE
+deu, ITA
+deu, ITA
+deu, LIE
+deu, LUX
+spa,
+spa, 419
+spa, ARG,
+spa, BRA
+fra,
+fra, BEL,
+fra, FRA,
+
+E Failed to check TTS data, no activity found for Intent
+{ act=android.speech.tts.engine.CHECK_TTS_DATA pkg=com.k2fsa.sherpa.chapter5 })
+
+E Failed to get default language from engine com.k2fsa.sherpa.chapter5
+Engine failed voice data integrity check (null return)com.k2fsa.sherpa.chapter5
+Failed to get default language from engine com.k2fsa.sherpa.chapter5
+
+*/
+
+class TtsService : TextToSpeechService() {
+ override fun onCreate() {
+ super.onCreate()
+
+ // see https://github.com/Miserlou/Android-SDK-Samples/blob/master/TtsEngine/src/com/example/android/ttsengine/RobotSpeakTtsService.java#L68
+ onLoadLanguage(TtsEngine.lang, "", "")
+ }
+
+ // https://developer.android.com/reference/kotlin/android/speech/tts/TextToSpeechService#onislanguageavailable
+ override fun onIsLanguageAvailable(_lang: String?, _country: String?, _variant: String?): Int {
+ val lang = _lang ?: ""
+
+ if (lang == TtsEngine.lang) {
+ return TextToSpeech.LANG_AVAILABLE
+ }
+
+ return TextToSpeech.LANG_NOT_SUPPORTED
+ }
+
+ override fun onGetLanguage(): Array {
+ return arrayOf(TtsEngine.lang!!, "", "")
+ }
+
+ // https://developer.android.com/reference/kotlin/android/speech/tts/TextToSpeechService#onLoadLanguage(kotlin.String,%20kotlin.String,%20kotlin.String)
+ override fun onLoadLanguage(_lang: String?, _country: String?, _variant: String?): Int {
+ val lang = _lang ?: ""
+
+ if (lang == TtsEngine.lang) {
+ TtsEngine.createTts(application)
+ return TextToSpeech.LANG_AVAILABLE
+ } else {
+ return TextToSpeech.LANG_NOT_SUPPORTED
+ }
+ }
+
+ override fun onStop() {}
+
+ override fun onSynthesizeText(request: SynthesisRequest?, callback: SynthesisCallback?) {
+ if (request == null || callback == null) {
+ return
+ }
+ val language = request.language
+ val country = request.country
+ val variant = request.variant
+ val text = request.charSequenceText.toString()
+
+ val ret = onIsLanguageAvailable(language, country, variant)
+ if (ret == TextToSpeech.LANG_NOT_SUPPORTED) {
+ callback.error()
+ return
+ }
+ Log.i(TAG, "text: $text")
+ val tts = TtsEngine.tts!!
+
+ // Note that AudioFormat.ENCODING_PCM_FLOAT requires API level >= 24
+ // callback.start(tts.sampleRate(), AudioFormat.ENCODING_PCM_FLOAT, 1)
+
+ callback.start(tts.sampleRate(), AudioFormat.ENCODING_PCM_16BIT, 1)
+
+ if (text.isBlank() || text.isEmpty()) {
+ callback.done()
+ return
+ }
+
+ val ttsCallback = {floatSamples: FloatArray ->
+ // convert FloatArray to ByteArray
+ val samples = floatArrayToByteArray(floatSamples)
+ val maxBufferSize: Int = callback.maxBufferSize
+ var offset = 0
+ while (offset < samples.size) {
+ val bytesToWrite = Math.min(maxBufferSize, samples.size - offset)
+ callback.audioAvailable(samples, offset, bytesToWrite)
+ offset += bytesToWrite
+ }
+
+ }
+
+ Log.i(TAG, "text: $text")
+ tts.generateWithCallback(
+ text = text,
+ sid = TtsEngine.speakerId,
+ speed = TtsEngine.speed,
+ callback=ttsCallback,
+ )
+
+ callback.done()
+ }
+
+ private fun floatArrayToByteArray(audio: FloatArray): ByteArray {
+ // byteArray is actually a ShortArray
+ val byteArray = ByteArray(audio.size * 2)
+ for (i in audio.indices) {
+ val sample = (audio[i] * 32767).toInt()
+ byteArray[2 * i] = sample.toByte()
+ byteArray[2 * i + 1] = (sample shr 8).toByte()
+ }
+ return byteArray
+ }
+}
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/ui/theme/Color.kt b/android/SherpaOnnxTtsEngine/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/ui/theme/Color.kt
new file mode 100644
index 0000000000..9af2bb2621
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/ui/theme/Color.kt
@@ -0,0 +1,11 @@
+package com.k2fsa.sherpa.onnx.tts.engine.ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+val Purple80 = Color(0xFFD0BCFF)
+val PurpleGrey80 = Color(0xFFCCC2DC)
+val Pink80 = Color(0xFFEFB8C8)
+
+val Purple40 = Color(0xFF6650a4)
+val PurpleGrey40 = Color(0xFF625b71)
+val Pink40 = Color(0xFF7D5260)
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/ui/theme/Theme.kt b/android/SherpaOnnxTtsEngine/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/ui/theme/Theme.kt
new file mode 100644
index 0000000000..799b1918ec
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/ui/theme/Theme.kt
@@ -0,0 +1,70 @@
+package com.k2fsa.sherpa.onnx.tts.engine.ui.theme
+
+import android.app.Activity
+import android.os.Build
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.SideEffect
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalView
+import androidx.core.view.WindowCompat
+
+private val DarkColorScheme = darkColorScheme(
+ primary = Purple80,
+ secondary = PurpleGrey80,
+ tertiary = Pink80
+)
+
+private val LightColorScheme = lightColorScheme(
+ primary = Purple40,
+ secondary = PurpleGrey40,
+ tertiary = Pink40
+
+ /* Other default colors to override
+ background = Color(0xFFFFFBFE),
+ surface = Color(0xFFFFFBFE),
+ onPrimary = Color.White,
+ onSecondary = Color.White,
+ onTertiary = Color.White,
+ onBackground = Color(0xFF1C1B1F),
+ onSurface = Color(0xFF1C1B1F),
+ */
+)
+
+@Composable
+fun SherpaOnnxTtsEngineTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ // Dynamic color is available on Android 12+
+ dynamicColor: Boolean = true,
+ content: @Composable () -> Unit
+) {
+ val colorScheme = when {
+ dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
+ val context = LocalContext.current
+ if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
+ }
+
+ darkTheme -> DarkColorScheme
+ else -> LightColorScheme
+ }
+ val view = LocalView.current
+ if (!view.isInEditMode) {
+ SideEffect {
+ val window = (view.context as Activity).window
+ window.statusBarColor = colorScheme.primary.toArgb()
+ WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
+ }
+ }
+
+ MaterialTheme(
+ colorScheme = colorScheme,
+ typography = Typography,
+ content = content
+ )
+}
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/ui/theme/Type.kt b/android/SherpaOnnxTtsEngine/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/ui/theme/Type.kt
new file mode 100644
index 0000000000..e66166931f
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/ui/theme/Type.kt
@@ -0,0 +1,34 @@
+package com.k2fsa.sherpa.onnx.tts.engine.ui.theme
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+// Set of Material typography styles to start with
+val Typography = Typography(
+ bodyLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.5.sp
+ )
+ /* Other default text styles to override
+ titleLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 22.sp,
+ lineHeight = 28.sp,
+ letterSpacing = 0.sp
+ ),
+ labelSmall = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Medium,
+ fontSize = 11.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.5.sp
+ )
+ */
+)
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine/app/src/main/jniLibs/arm64-v8a/.gitkeep b/android/SherpaOnnxTtsEngine/app/src/main/jniLibs/arm64-v8a/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/android/SherpaOnnxTtsEngine/app/src/main/jniLibs/armeabi-v7a/.gitkeep b/android/SherpaOnnxTtsEngine/app/src/main/jniLibs/armeabi-v7a/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/android/SherpaOnnxTtsEngine/app/src/main/jniLibs/x86/.gitkeep b/android/SherpaOnnxTtsEngine/app/src/main/jniLibs/x86/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/android/SherpaOnnxTtsEngine/app/src/main/jniLibs/x86_64/.gitkeep b/android/SherpaOnnxTtsEngine/app/src/main/jniLibs/x86_64/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/android/SherpaOnnxTtsEngine/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/android/SherpaOnnxTtsEngine/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000000..2b068d1146
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine/app/src/main/res/drawable/ic_launcher_background.xml b/android/SherpaOnnxTtsEngine/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000000..07d5da9cbf
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/SherpaOnnxTtsEngine/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/SherpaOnnxTtsEngine/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000000..6f3b755bf5
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/SherpaOnnxTtsEngine/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000000..6f3b755bf5
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/android/SherpaOnnxTtsEngine/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000000..c209e78ecd
Binary files /dev/null and b/android/SherpaOnnxTtsEngine/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/android/SherpaOnnxTtsEngine/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/android/SherpaOnnxTtsEngine/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000000..b2dfe3d1ba
Binary files /dev/null and b/android/SherpaOnnxTtsEngine/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/android/SherpaOnnxTtsEngine/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/android/SherpaOnnxTtsEngine/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000000..4f0f1d64e5
Binary files /dev/null and b/android/SherpaOnnxTtsEngine/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/android/SherpaOnnxTtsEngine/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/android/SherpaOnnxTtsEngine/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000000..62b611da08
Binary files /dev/null and b/android/SherpaOnnxTtsEngine/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/android/SherpaOnnxTtsEngine/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/android/SherpaOnnxTtsEngine/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000000..948a3070fe
Binary files /dev/null and b/android/SherpaOnnxTtsEngine/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/android/SherpaOnnxTtsEngine/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/android/SherpaOnnxTtsEngine/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000000..1b9a6956b3
Binary files /dev/null and b/android/SherpaOnnxTtsEngine/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/android/SherpaOnnxTtsEngine/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/android/SherpaOnnxTtsEngine/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000000..28d4b77f9f
Binary files /dev/null and b/android/SherpaOnnxTtsEngine/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/android/SherpaOnnxTtsEngine/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/android/SherpaOnnxTtsEngine/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000000..9287f50836
Binary files /dev/null and b/android/SherpaOnnxTtsEngine/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/android/SherpaOnnxTtsEngine/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/android/SherpaOnnxTtsEngine/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000000..aa7d6427e6
Binary files /dev/null and b/android/SherpaOnnxTtsEngine/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/android/SherpaOnnxTtsEngine/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/android/SherpaOnnxTtsEngine/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000000..9126ae37cb
Binary files /dev/null and b/android/SherpaOnnxTtsEngine/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/android/SherpaOnnxTtsEngine/app/src/main/res/values/colors.xml b/android/SherpaOnnxTtsEngine/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000000..f8c6127d32
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine/app/src/main/res/values/colors.xml
@@ -0,0 +1,10 @@
+
+
+ #FFBB86FC
+ #FF6200EE
+ #FF3700B3
+ #FF03DAC5
+ #FF018786
+ #FF000000
+ #FFFFFFFF
+
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine/app/src/main/res/values/strings.xml b/android/SherpaOnnxTtsEngine/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..f114254f0a
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine/app/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ Next-gen Kaldi: TTS
+
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine/app/src/main/res/values/themes.xml b/android/SherpaOnnxTtsEngine/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000000..012ac97063
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine/app/src/main/res/values/themes.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine/app/src/main/res/xml/backup_rules.xml b/android/SherpaOnnxTtsEngine/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000000..fa0f996d2c
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine/app/src/main/res/xml/data_extraction_rules.xml b/android/SherpaOnnxTtsEngine/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000000..9ee9997b0b
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine/app/src/main/res/xml/tts_engine.xml b/android/SherpaOnnxTtsEngine/app/src/main/res/xml/tts_engine.xml
new file mode 100644
index 0000000000..3f13baeffc
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine/app/src/main/res/xml/tts_engine.xml
@@ -0,0 +1,5 @@
+
+
+
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine/app/src/test/java/com/k2fsa/sherpa/onnx/tts/engine/ExampleUnitTest.kt b/android/SherpaOnnxTtsEngine/app/src/test/java/com/k2fsa/sherpa/onnx/tts/engine/ExampleUnitTest.kt
new file mode 100644
index 0000000000..0132285561
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine/app/src/test/java/com/k2fsa/sherpa/onnx/tts/engine/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.k2fsa.sherpa.onnx.tts.engine
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine/build.gradle.kts b/android/SherpaOnnxTtsEngine/build.gradle.kts
new file mode 100644
index 0000000000..8e8f4ab912
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine/build.gradle.kts
@@ -0,0 +1,5 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+ id("com.android.application") version "8.2.0" apply false
+ id("org.jetbrains.kotlin.android") version "1.9.0" apply false
+}
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine/gradle.properties b/android/SherpaOnnxTtsEngine/gradle.properties
new file mode 100644
index 0000000000..3c5031eb7d
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine/gradle.properties
@@ -0,0 +1,23 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine/gradle/wrapper/gradle-wrapper.jar b/android/SherpaOnnxTtsEngine/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000000..e708b1c023
Binary files /dev/null and b/android/SherpaOnnxTtsEngine/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/android/SherpaOnnxTtsEngine/gradle/wrapper/gradle-wrapper.properties b/android/SherpaOnnxTtsEngine/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000000..83ff778c5b
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Sun Dec 31 18:47:53 CST 2023
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/android/SherpaOnnxTtsEngine/gradlew b/android/SherpaOnnxTtsEngine/gradlew
new file mode 100755
index 0000000000..4f906e0c81
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/android/SherpaOnnxTtsEngine/gradlew.bat b/android/SherpaOnnxTtsEngine/gradlew.bat
new file mode 100644
index 0000000000..ac1b06f938
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/android/SherpaOnnxTtsEngine/settings.gradle.kts b/android/SherpaOnnxTtsEngine/settings.gradle.kts
new file mode 100644
index 0000000000..df556b8080
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine/settings.gradle.kts
@@ -0,0 +1,17 @@
+pluginManagement {
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.name = "SherpaOnnxTtsEngine"
+include(":app")
diff --git a/sherpa-onnx/csrc/offline-tts-impl.h b/sherpa-onnx/csrc/offline-tts-impl.h
index f5134aa727..3c9e27b1bf 100644
--- a/sherpa-onnx/csrc/offline-tts-impl.h
+++ b/sherpa-onnx/csrc/offline-tts-impl.h
@@ -34,6 +34,10 @@ class OfflineTtsImpl {
// Return the sample rate of the generated audio
virtual int32_t SampleRate() const = 0;
+
+ // Number of supported speakers.
+ // If it supports only a single speaker, then it return 0 or 1.
+ virtual int32_t NumSpeakers() const = 0;
};
} // namespace sherpa_onnx
diff --git a/sherpa-onnx/csrc/offline-tts-vits-impl.h b/sherpa-onnx/csrc/offline-tts-vits-impl.h
index 4ac12bab65..978f557c34 100644
--- a/sherpa-onnx/csrc/offline-tts-vits-impl.h
+++ b/sherpa-onnx/csrc/offline-tts-vits-impl.h
@@ -74,6 +74,10 @@ class OfflineTtsVitsImpl : public OfflineTtsImpl {
return model_->GetMetaData().sample_rate;
}
+ int32_t NumSpeakers() const override {
+ return model_->GetMetaData().num_speakers;
+ }
+
GeneratedAudio Generate(
const std::string &_text, int64_t sid = 0, float speed = 1.0,
GeneratedAudioCallback callback = nullptr) const override {
diff --git a/sherpa-onnx/csrc/offline-tts.cc b/sherpa-onnx/csrc/offline-tts.cc
index ddb649a208..6f7e472a23 100644
--- a/sherpa-onnx/csrc/offline-tts.cc
+++ b/sherpa-onnx/csrc/offline-tts.cc
@@ -73,4 +73,6 @@ GeneratedAudio OfflineTts::Generate(
int32_t OfflineTts::SampleRate() const { return impl_->SampleRate(); }
+int32_t OfflineTts::NumSpeakers() const { return impl_->NumSpeakers(); }
+
} // namespace sherpa_onnx
diff --git a/sherpa-onnx/csrc/offline-tts.h b/sherpa-onnx/csrc/offline-tts.h
index bbc9013c29..f67b20b588 100644
--- a/sherpa-onnx/csrc/offline-tts.h
+++ b/sherpa-onnx/csrc/offline-tts.h
@@ -86,6 +86,10 @@ class OfflineTts {
// Return the sample rate of the generated audio
int32_t SampleRate() const;
+ // Number of supported speakers.
+ // If it supports only a single speaker, then it return 0 or 1.
+ int32_t NumSpeakers() const;
+
private:
std::unique_ptr impl_;
};
diff --git a/sherpa-onnx/jni/jni.cc b/sherpa-onnx/jni/jni.cc
index 9dada00548..a5e194107b 100644
--- a/sherpa-onnx/jni/jni.cc
+++ b/sherpa-onnx/jni/jni.cc
@@ -524,6 +524,8 @@ class SherpaOnnxOfflineTts {
int32_t SampleRate() const { return tts_.SampleRate(); }
+ int32_t NumSpeakers() const { return tts_.NumSpeakers(); }
+
private:
OfflineTts tts_;
};
@@ -652,6 +654,13 @@ JNIEXPORT jint JNICALL Java_com_k2fsa_sherpa_onnx_OfflineTts_getSampleRate(
->SampleRate();
}
+SHERPA_ONNX_EXTERN_C
+JNIEXPORT jint JNICALL Java_com_k2fsa_sherpa_onnx_OfflineTts_getNumSpeakers(
+ JNIEnv *env, jobject /*obj*/, jlong ptr) {
+ return reinterpret_cast(ptr)
+ ->NumSpeakers();
+}
+
// see
// https://stackoverflow.com/questions/29043872/android-jni-return-multiple-variables
static jobject NewInteger(JNIEnv *env, int32_t value) {
diff --git a/sherpa-onnx/python/csrc/offline-tts.cc b/sherpa-onnx/python/csrc/offline-tts.cc
index d98d40a310..144001b0be 100644
--- a/sherpa-onnx/python/csrc/offline-tts.cc
+++ b/sherpa-onnx/python/csrc/offline-tts.cc
@@ -51,6 +51,7 @@ void PybindOfflineTts(py::module *m) {
.def(py::init(), py::arg("config"),
py::call_guard())
.def_property_readonly("sample_rate", &PyClass::SampleRate)
+ .def_property_readonly("num_speakers", &PyClass::NumSpeakers)
.def(
"generate",
[](const PyClass &self, const std::string &text, int64_t sid,