Android TTS stops reading aloud after the screen is turned off for a while

2k views Asked by At

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?

3

There are 3 answers

4
Kanzariya Hardik On

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
Tom Ford On

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.

0
Denny Hsu On

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.