Text to speech over Bluetooth gets cut off at the starting. How to fix it?

Viewed 222

I've been working on a navigation feature for a maps app which has voice instructions. The problem is that when announcing the instructions, the first 500 milliseconds of the instruction gets cut off. For eg, if the instruction is "In 200m turn right", in the bluetooth earphone it ends up sounding like "200m turn right". Or if the instruction is "Continue for 2 kilometers", then it sounds like "tinue for 2 kilometers".

This is the code I'm using for TTS -

//Initialisation happens only once
var textToSpeechEngine = TextToSpeech(this) { status ->
    if (status == TextToSpeech.SUCCESS) {
        textToSpeechEngine?.language = Locale.ENGLISH
        textToSpeechEngine?.setSpeechRate(0.8f)
    }
}
...
//When text to speak is ready, invoking the speak method
textToSpeechEngine?.speak(textToSpeak, TextToSpeech.QUEUE_FLUSH, null, "tts1")

Additionally, I'm also using AudioFocusRequest to request and abandon focus so that any music playing in the background lowers its volume while the instruction is being spoken. This is the code I'm using for that -

val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
var focusRequest: AudioFocusRequest? = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK)
            .setAudioAttributes(
                    AudioAttributes.Builder()
                            .setUsage(AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
                            .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
                            .build())
            .setAcceptsDelayedFocusGain(false)
            .build()
    audioManager.requestAudioFocus(focusRequest)
} else {
    audioManager.requestAudioFocus(null, AudioManager.STREAM_NOTIFICATION, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
}

textToSpeechEngine?.setOnUtteranceProgressListener(object : UtteranceProgressListener() {
    fun abandonFocus() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            focusRequest?.let { request -> audioManager.abandonAudioFocusRequest(request) }
        } else {
            audioManager.abandonAudioFocus(null)
        }
    }

    override fun onStart(utteranceId: String?) {}
    override fun onDone(utteranceId: String?) {
        abandonFocus()
    }

    override fun onError(utteranceId: String?) {
        abandonFocus()
    }
})

Point to note here is that this issue is not happening in all the bluetooth devices I've tested with. The issue happened with 2 Chinese brands bluetooth earphones but is not happening with a bluetooth Sony over-the-head headset. Also, the issue does not happen if music is playing in the background while navigation is going on.

As I understand it, it seems that the cheaper bluetooth earphones seem to keep the "connection alive" only when audio is actively coming through else it stops the connection temporarily in order to save battery I guess? However, when music is playing in the background, the connection is kept alive constantly so the instruction speech does not get cut off.

What can I do to fix this or work around it?

2 Answers

note that requestAudioFocus may take a OnAudioFocusChangeListener as first param, you are passing null in there (also Builder have this param). switching focus may take some (short) time, so I would recomend to fire your TTS when you get this callback fired with AUDIOFOCUS_GAIN

Assuming that you're correct that it is purely hardware at fault (which is what it seems like):

You can use the playSilence() or playSilentUtterance() methods of the TextToSpeech class to play silence for 500ms prior to your main speak() command... which should fool the speakers/headphones.

It may help to also use QUEUE_ADD instead of QUEUE_FLUSH for your main speak() request to make sure that it it attached the the previous (silence) with no gap... and that it doesn't prematurely end the first (silent) utterance.

There are a lot of variables at play here. You could consider:

  • Can you find an app that uses TextToSpeech which DOES work correctly even on these problematic devices? If so, then the problem must be solvable in code, and maybe you could find the source code for the app you tested and look at how they are setting up the AudioManager.
Related