Text to speech for usernames
This commit is contained in:
@@ -0,0 +1,164 @@
|
|||||||
|
/*
|
||||||
|
* Created by Ayaan on 02/02/23, 10:30 pm
|
||||||
|
* Copyright (c) 2023 . All rights reserved.
|
||||||
|
* Last modified 02/02/23, 9:56 pm
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.vitorpamplona.amethyst.service.tts
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.speech.tts.TextToSpeech
|
||||||
|
import android.speech.tts.UtteranceProgressListener
|
||||||
|
import android.util.Log
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
const val DEF_SPEECH_AND_PITCH = 0.8f
|
||||||
|
|
||||||
|
fun getErrorText(errorCode: Int): String = when (errorCode) {
|
||||||
|
TextToSpeech.ERROR -> "ERROR"
|
||||||
|
TextToSpeech.ERROR_INVALID_REQUEST -> "ERROR_INVALID_REQUEST"
|
||||||
|
TextToSpeech.ERROR_NETWORK -> "ERROR_NETWORK"
|
||||||
|
TextToSpeech.ERROR_NETWORK_TIMEOUT -> "ERROR_NETWORK_TIMEOUT"
|
||||||
|
TextToSpeech.ERROR_SERVICE -> "ERROR_SERVICE"
|
||||||
|
TextToSpeech.ERROR_SYNTHESIS -> "ERROR_SYNTHESIS"
|
||||||
|
TextToSpeech.ERROR_NOT_INSTALLED_YET -> "ERROR_NOT_INSTALLED_YET"
|
||||||
|
else -> "UNKNOWN"
|
||||||
|
}
|
||||||
|
|
||||||
|
class TextToSpeechEngine private constructor() {
|
||||||
|
private var tts: TextToSpeech? = null
|
||||||
|
|
||||||
|
private var defaultPitch = 0.8f
|
||||||
|
private var defaultSpeed = 0.8f
|
||||||
|
private var defLanguage = Locale.getDefault()
|
||||||
|
private var onStartListener: (() -> Unit)? = null
|
||||||
|
private var onDoneListener: (() -> Unit)? = null
|
||||||
|
private var onErrorListener: ((String) -> Unit)? = null
|
||||||
|
private var onHighlightListener: ((Int, Int) -> Unit)? = null
|
||||||
|
private var message: String? = null
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private var instance: TextToSpeechEngine? = null
|
||||||
|
fun getInstance(): TextToSpeechEngine {
|
||||||
|
if (instance == null) {
|
||||||
|
instance = TextToSpeechEngine()
|
||||||
|
}
|
||||||
|
return instance!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun initTTS(context: Context, message: String) {
|
||||||
|
tts = TextToSpeech(context) {
|
||||||
|
if (it == TextToSpeech.SUCCESS) {
|
||||||
|
tts!!.language = defLanguage
|
||||||
|
tts!!.setPitch(defaultPitch)
|
||||||
|
tts!!.setSpeechRate(defaultSpeed)
|
||||||
|
tts!!.setListener(
|
||||||
|
onStart = {
|
||||||
|
onStartListener?.invoke()
|
||||||
|
},
|
||||||
|
onError = { e ->
|
||||||
|
e?.let { error ->
|
||||||
|
onErrorListener?.invoke(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRange = { start, end ->
|
||||||
|
if (this@TextToSpeechEngine.message != null) {
|
||||||
|
onHighlightListener?.invoke(start, end)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDone = {
|
||||||
|
onStartListener?.invoke()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
speak(message)
|
||||||
|
} else {
|
||||||
|
Log.d("AAA", "initTTS: $it")
|
||||||
|
onErrorListener?.invoke(getErrorText(it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun speak(message: String): TextToSpeechEngine {
|
||||||
|
tts!!.speak(
|
||||||
|
message,
|
||||||
|
TextToSpeech.QUEUE_FLUSH,
|
||||||
|
null,
|
||||||
|
TextToSpeech.ACTION_TTS_QUEUE_PROCESSING_COMPLETED
|
||||||
|
)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setPitchAndSpeed(pitch: Float, speed: Float) {
|
||||||
|
defaultPitch = pitch
|
||||||
|
defaultSpeed = speed
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resetPitchAndSpeed() {
|
||||||
|
defaultPitch = DEF_SPEECH_AND_PITCH
|
||||||
|
defaultSpeed = DEF_SPEECH_AND_PITCH
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setLanguage(local: Locale): TextToSpeechEngine {
|
||||||
|
this.defLanguage = local
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setHighlightedMessage(message: String) {
|
||||||
|
this.message = message
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setOnStartListener(onStartListener: (() -> Unit)): TextToSpeechEngine {
|
||||||
|
this.onStartListener = onStartListener
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setOnCompletionListener(onDoneListener: () -> Unit): TextToSpeechEngine {
|
||||||
|
this.onDoneListener = onDoneListener
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setOnErrorListener(onErrorListener: (String) -> Unit): TextToSpeechEngine {
|
||||||
|
this.onErrorListener = onErrorListener
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setOnHighlightListener(onHighlightListener: (Int, Int) -> Unit): TextToSpeechEngine {
|
||||||
|
this.onHighlightListener = onHighlightListener
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun destroy() {
|
||||||
|
tts?.stop()
|
||||||
|
tts?.shutdown()
|
||||||
|
tts = null
|
||||||
|
instance = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun TextToSpeech.setListener(
|
||||||
|
crossinline onStart: (String?) -> Unit = {},
|
||||||
|
crossinline onError: (String?) -> Unit = {},
|
||||||
|
crossinline onRange: (Int, Int) -> Unit = { _, _ -> },
|
||||||
|
crossinline onDone: (String?) -> Unit
|
||||||
|
) = this.apply {
|
||||||
|
setOnUtteranceProgressListener(object : UtteranceProgressListener() {
|
||||||
|
override fun onStart(p0: String?) {
|
||||||
|
onStart.invoke(p0)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDone(p0: String?) {
|
||||||
|
onDone.invoke(p0)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated("Deprecated in Java", ReplaceWith("onError.invoke(p0)"))
|
||||||
|
override fun onError(p0: String?) {
|
||||||
|
onError.invoke(p0)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRangeStart(utteranceId: String?, start: Int, end: Int, frame: Int) {
|
||||||
|
super.onRangeStart(utteranceId, start, end, frame)
|
||||||
|
onRange.invoke(start, end)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
/*
|
||||||
|
* Created by Ayaan on 02/02/23, 10:30 pm
|
||||||
|
* Copyright (c) 2023 . All rights reserved.
|
||||||
|
* Last modified 02/02/23, 10:19 pm
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.vitorpamplona.amethyst.service.tts
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class TextToSpeechHelper private constructor(private val context: WeakReference<Context>) :
|
||||||
|
LifecycleEventObserver {
|
||||||
|
private val appContext
|
||||||
|
get() = context.get()!!.applicationContext
|
||||||
|
|
||||||
|
private var message: String? = null
|
||||||
|
|
||||||
|
private var tts: TextToSpeechEngine? = null
|
||||||
|
|
||||||
|
private var onStart: (() -> Unit)? = null
|
||||||
|
|
||||||
|
private var onDoneListener: (() -> Unit)? = null
|
||||||
|
|
||||||
|
private var onErrorListener: ((String) -> Unit)? = null
|
||||||
|
|
||||||
|
private var onHighlightListener: ((Pair<Int, Int>) -> Unit)? = null
|
||||||
|
|
||||||
|
private var customActionForDestroy: (() -> Unit)? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
initTTS()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun registerLifecycle(owner: LifecycleOwner): TextToSpeechHelper {
|
||||||
|
owner.lifecycle.addObserver(this)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initTTS() = context.get()?.run {
|
||||||
|
tts = TextToSpeechEngine.getInstance()
|
||||||
|
.setOnCompletionListener { onDoneListener?.invoke() }
|
||||||
|
.setOnErrorListener { onErrorListener?.invoke(it) }
|
||||||
|
.setOnStartListener { onStart?.invoke() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun speak(message: String): TextToSpeechHelper {
|
||||||
|
if (tts == null) {
|
||||||
|
initTTS()
|
||||||
|
}
|
||||||
|
this.message = message
|
||||||
|
|
||||||
|
tts?.initTTS(
|
||||||
|
appContext,
|
||||||
|
message
|
||||||
|
)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method will highlight the text in the textView
|
||||||
|
*
|
||||||
|
* @exception Exception("Message can't be null for highlighting !! Call speak() first")
|
||||||
|
*/
|
||||||
|
fun highlight(): TextToSpeechHelper {
|
||||||
|
if (message == null) throw Exception("Message can't be null for highlighting !! Call speak() first")
|
||||||
|
tts?.setHighlightedMessage(message!!)
|
||||||
|
tts?.setOnHighlightListener { i, i2 ->
|
||||||
|
onHighlightListener?.invoke(Pair(i, i2))
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeHighlight(): TextToSpeechHelper {
|
||||||
|
message = null
|
||||||
|
onHighlightListener = null
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun destroy(
|
||||||
|
action: (() -> Unit) = {}
|
||||||
|
) {
|
||||||
|
tts?.destroy()
|
||||||
|
tts = null
|
||||||
|
action.invoke()
|
||||||
|
INSTANCE = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onStart(onStartListener: () -> Unit): TextToSpeechHelper {
|
||||||
|
this.onStart = onStartListener
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onDone(onCompleteListener: () -> Unit): TextToSpeechHelper {
|
||||||
|
this.onDoneListener = onCompleteListener
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onError(onErrorListener: (String) -> Unit): TextToSpeechHelper {
|
||||||
|
this.onErrorListener = onErrorListener
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onHighlight(onHighlightListener: (Pair<Int, Int>) -> Unit): TextToSpeechHelper {
|
||||||
|
this.onHighlightListener = onHighlightListener
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setCustomActionForDestroy(action: () -> Unit): TextToSpeechHelper {
|
||||||
|
customActionForDestroy = action
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setLanguage(locale: Locale): TextToSpeechHelper {
|
||||||
|
tts?.setLanguage(locale)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setPitchAndSpeed(
|
||||||
|
pitch: Float = DEF_SPEECH_AND_PITCH,
|
||||||
|
speed: Float = DEF_SPEECH_AND_PITCH
|
||||||
|
): TextToSpeechHelper {
|
||||||
|
tts?.setPitchAndSpeed(pitch, speed)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resetPitchAndSpeed(): TextToSpeechHelper {
|
||||||
|
tts?.resetPitchAndSpeed()
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private var INSTANCE: TextToSpeechHelper? = null
|
||||||
|
fun getInstance(context: Context): TextToSpeechHelper {
|
||||||
|
synchronized(TextToSpeechHelper::class.java) {
|
||||||
|
if (INSTANCE == null) {
|
||||||
|
INSTANCE = TextToSpeechHelper(WeakReference(context))
|
||||||
|
}
|
||||||
|
return INSTANCE!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
|
||||||
|
if (event == Lifecycle.Event.ON_DESTROY ||
|
||||||
|
event == Lifecycle.Event.ON_STOP ||
|
||||||
|
event == Lifecycle.Event.ON_PAUSE
|
||||||
|
) {
|
||||||
|
destroy {
|
||||||
|
customActionForDestroy?.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,28 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.note
|
package com.vitorpamplona.amethyst.ui.note
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.IconButton
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.PlayCircle
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
|
import com.vitorpamplona.amethyst.service.tts.TextToSpeechHelper
|
||||||
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
|
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
|
||||||
import com.vitorpamplona.amethyst.ui.actions.toImmutableListOfLists
|
import com.vitorpamplona.amethyst.ui.actions.toImmutableListOfLists
|
||||||
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||||
@@ -45,6 +57,9 @@ private fun UserNameDisplay(
|
|||||||
tags: ImmutableListOfLists<String>?,
|
tags: ImmutableListOfLists<String>?,
|
||||||
modifier: Modifier
|
modifier: Modifier
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
|
|
||||||
if (bestUserName != null && bestDisplayName != null) {
|
if (bestUserName != null && bestDisplayName != null) {
|
||||||
CreateTextWithEmoji(
|
CreateTextWithEmoji(
|
||||||
text = bestDisplayName,
|
text = bestDisplayName,
|
||||||
@@ -59,6 +74,17 @@ private fun UserNameDisplay(
|
|||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
|
IconButton(
|
||||||
|
onClick = { speak(bestDisplayName, context, lifecycleOwner) },
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.PlayCircle,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
tint = MaterialTheme.colors.placeholderText
|
||||||
|
)
|
||||||
|
}
|
||||||
} else if (bestDisplayName != null) {
|
} else if (bestDisplayName != null) {
|
||||||
CreateTextWithEmoji(
|
CreateTextWithEmoji(
|
||||||
text = bestDisplayName,
|
text = bestDisplayName,
|
||||||
@@ -68,6 +94,17 @@ private fun UserNameDisplay(
|
|||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
|
IconButton(
|
||||||
|
onClick = { speak(bestDisplayName, context, lifecycleOwner) },
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.PlayCircle,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
tint = MaterialTheme.colors.placeholderText
|
||||||
|
)
|
||||||
|
}
|
||||||
} else if (bestUserName != null) {
|
} else if (bestUserName != null) {
|
||||||
CreateTextWithEmoji(
|
CreateTextWithEmoji(
|
||||||
text = remember { "@$bestUserName" },
|
text = remember { "@$bestUserName" },
|
||||||
@@ -77,6 +114,17 @@ private fun UserNameDisplay(
|
|||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
|
IconButton(
|
||||||
|
onClick = { speak(bestUserName, context, lifecycleOwner) },
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.PlayCircle,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
tint = MaterialTheme.colors.placeholderText
|
||||||
|
)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Text(
|
Text(
|
||||||
npubDisplay,
|
npubDisplay,
|
||||||
@@ -87,3 +135,21 @@ private fun UserNameDisplay(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun speak(
|
||||||
|
message: String,
|
||||||
|
context: Context,
|
||||||
|
owner: LifecycleOwner
|
||||||
|
) {
|
||||||
|
TextToSpeechHelper
|
||||||
|
.getInstance(context)
|
||||||
|
.registerLifecycle(owner)
|
||||||
|
.speak(message)
|
||||||
|
.highlight()
|
||||||
|
.onDone {
|
||||||
|
Log.d("TextToSpeak", "speak: done")
|
||||||
|
}
|
||||||
|
.onError {
|
||||||
|
Log.d("TextToSpeak", "speak error: $it")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user