3

I am working on an app that needs to keep reading aloud text after the screen is turned off. To achieve this goal, I put the Text-to-speech (TTS) code in the foreground service, so the TTS can keep running when the screen is off.

It worked well on my phone before. But after I upgraded my phone from Android 11 to Android 12, the TTS stops working after the screen is turned off for a while, usually after several minutes.

Normally, after the TTS finishes speaking one sentence, it will call the onDone method of the UtteranceProgressListener, so I can make the TTS speak next sentence there. The reason the TTS stops working is that the onDone method stops getting called after the screen is turned off for a while. It doesn't stop immediately, but stops after a few minutes, sometimes longer, sometimes shorter.

EDIT:

At the beginning I turn off the battery optimization for the whole system, but it doesn't work. Then I turn off the battery optimization for a single app. I need to go to the settings for a single app and turn it off, or do it programmatically like this:

Check if battery optimization is enabled or not for an app

This issue is greatly improved after I turn off the battery optimization for a single app. However, the TTS still stops once for a while, about once for several hours. I also notice that the app "T2S" can keep running even when its battery optimization is on. What can I do to let TTS keep running when the battery optimization is on, just like what "T2S" does, or at least don't let it stop after battery optimization is off?

Denny Hsu
  • 301
  • 2
  • 10

3 Answers3

2

To add on to Denny Hsu's answer:

Each tts engine has a maximum amount of characters it will sythesize as speech at a time. You can find this maximum number with the following:

TextToSpeech.getMaxSpeechInputLength()

For example, I believe the Google TTS Engine's max is 4000 chars.

You can counter this by either stopping the user from attempting to speak text over this limit, or by breaking the text up into separate strings at or before this limit, and sending them sequentially.

In the latter case, best to find the end of the last sentence before the limit, and separate there to ensure that proper pronunciation is maintained.

Tom Ford
  • 23
  • 3
  • Thank you for adding more information. Even the input string length does not reach TTS Engine's max amount of characters, it may also stop TTS from speaking when screen is off. For example, a string length of about 300 is enough to stop it. Ensuring the proper pronunciation is maintained is also important. For some languages if you cut the sentence by comma, the pronunciation is different. – Denny Hsu Dec 04 '22 at 03:18
0

This code is working in Android 12 even app is background

class TTS : Service(), OnInitListener {

private var tts: TextToSpeech? = null
private lateinit var spokenText: String
private var isInit: Boolean = false

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
    if(intent?.extras != null) {
        spokenText = intent.getStringExtra("text").toString()
    }
    else {
        spokenText = ""
    }
    Log.d(TAG, "onStartCommand: $spokenText")
    return START_NOT_STICKY
}

override fun onCreate() {
    tts = TextToSpeech(this, this)
    Log.d(TAG, "onCreate: CREATING AGAIN !!")
}

override fun onInit(status: Int) {
    if (status == TextToSpeech.SUCCESS) {
        Log.d(TAG, "onInit: TextToSpeech Success")
        val result = tts!!.setLanguage(Locale("hi", "IN"))
        if (result != TextToSpeech.LANG_MISSING_DATA && result != TextToSpeech.LANG_NOT_SUPPORTED) {
            Log.d(TAG, "onInit: speaking........")
            addAudioAttributes()
            isInit = true
        }
    }
    else {
        Log.d(TAG, "onInit: TTS initialization failed")
        Toast.makeText(
            applicationContext,
            "Your device don't support text to speech.\n Visit app to download!!",
            Toast.LENGTH_SHORT
        ).show()
    }
}

private fun addAudioAttributes() {
    val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        val audioAttributes = AudioAttributes.Builder()
            .setUsage(AudioAttributes.USAGE_MEDIA)
            .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
            .build()
        tts?.setAudioAttributes(audioAttributes)
    }

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        val focusRequest =
            AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK)
                .setAudioAttributes(
                    AudioAttributes.Builder()
                        .setUsage(AudioAttributes.USAGE_MEDIA)
                        .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
                        .build()
                )
                .setAcceptsDelayedFocusGain(true)
                .setOnAudioFocusChangeListener { focus ->
                    when (focus) {
                        AudioManager.AUDIOFOCUS_GAIN -> {
                        }
                        else -> stopSelf()
                    }
                }.build()

        when (audioManager.requestAudioFocus(focusRequest)) {
            AudioManager.AUDIOFOCUS_REQUEST_GRANTED -> speak(audioManager, focusRequest)
            AudioManager.AUDIOFOCUS_REQUEST_DELAYED -> stopSelf()
            AudioManager.AUDIOFOCUS_REQUEST_FAILED -> stopSelf()
        }

    } else {
        val result = audioManager.requestAudioFocus( { focusChange: Int ->
            when(focusChange) {
                AudioManager.AUDIOFOCUS_GAIN -> {
                }
                else -> stopSelf()
            }
        },
            AudioManager.STREAM_MUSIC,
            AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
        )

        if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
            speak(audioManager, null)
        }
    }
}

private fun speak(audioManager: AudioManager, focusRequest: AudioFocusRequest?) {
    val speechListener = object : UtteranceProgressListener() {
        override fun onStart(utteranceId: String?) {
            Log.d(TAG, "onStart: Started syntheses.....")
        }

        override fun onDone(utteranceId: String?) {
            Log.d(TAG, "onDone: Completed synthesis ")
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && focusRequest != null) {
                audioManager.abandonAudioFocusRequest(focusRequest)
            }
            stopSelf()
        }

        override fun onError(utteranceId: String?) {
            Log.d(TAG, "onError: Error synthesis")
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && focusRequest != null) {
                audioManager.abandonAudioFocusRequest(focusRequest)
            }
            stopSelf()
        }
    }
    val paramsMap: HashMap<String, String> = HashMap()
    paramsMap[TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID] = "tts_service"

    tts?.speak(spokenText, TextToSpeech.QUEUE_ADD, paramsMap)
    tts?.setOnUtteranceProgressListener(speechListener)
}

override fun onDestroy() {
    if (tts != null) {
        Log.d(TAG, "onDestroy: destroyed tts")
        tts?.stop()
        tts?.shutdown()
    }
    super.onDestroy()
}

override fun onBind(arg0: Intent?): IBinder? {
    return null
}

companion object {
    private const val TAG = "TTS_Service"
}

}

  • 1
    I compared your code and found two key differences. You implement ```OnInitListener``` to the class and put ```this``` at the second parameter of ```TextToSpeech(this, this)```. I directly create an object in the function parentheses. Second, you create an ```UtteranceProgressListener``` first and put it in ```setOnUtteranceProgressListener()```. I directly create an object in the function parentheses. I change the coding style to be like yours, and I also need to delete a ```CountDownTimer``` which is also running in the Foreground Service. Now it works perfectly. Thank you for your help! – Denny Hsu May 10 '22 at 07:00
  • 1
    I did more tests today and found that I also need to add ```setAudioAttributes()``` to my code to prevent TTS from stopping. It seems that there are multiple reasons that TTS stops when screen is off. – Denny Hsu May 11 '22 at 15:17
  • The TTS object should only be created in ```onCreate()```, or it will stop after screen is off for a while. – Denny Hsu May 13 '22 at 08:12
  • I found that I didn't really disable the battery optimization before. After I disabled the battery optimization, the problem was solved. Now I can keep the ```CountDownTimer``` and create TTS at any place. The good coding style can make the TTS live longer but not forever, if the battery optimization is too aggressive. This post is the method to really disable the battery optimization. [Check if battery optimization is enabled or not for an app](https://stackoverflow.com/questions/39256501/check-if-battery-optimization-is-enabled-or-not-for-an-app) – Denny Hsu May 14 '22 at 10:23
0

The root cause that the TTS stops running once for a while even when the battery optimization is off is that the input text for speak() function is too long.

By the way, it seems that Android 13 has fixed the issue. No need to turn of battery optimization to make TTS keep running when screen is off.

Denny Hsu
  • 301
  • 2
  • 10