Friday, July 13, 2012

Android text-to-speech programming gotcha.

Implementing text-to-speech (TTS) on Android is quite straightforward.  There are tons of tutorial out there.  Just google "Android TTS tutorial" and you will find them.  However, many of them fail to point out a very important point that got me stumble for a while.

Like many Android service, TTS is shared among applications.  The Android reference says:

When you are done using the TextToSpeech instance, call the shutdown() method to release the native resources used by the TextToSpeech engine.

So, you want to be nice and release the TTS whenever your app get paused or killed.  To make thing simple, you would generally initialize a shared resource in onResume() because this function will be called both when your app is started and when it is resumed.  Similarly, you would release the shared resource in onPause(), since this function is called when the app got killed or paused.

However, if you do it with TTS, you are in trouble.  To understand why,  you need to know how to use TTS on Android first.

private void initTts() {
    Intent checkIntent = new Intent();
    checkIntent.setAction(TextToSpeech.Engine.ACTION_CHECK_TTS_DATA);
    startActivityForResult(checkIntent, MY_DATA_CHECK_CODE);
}

To use TTS on Android, you will need to check its availability before you can initialize it.  You check TTS availability by starting the checking activity as shown above.  When the checking activity returns, the function onActivityResult(...) is called and you can check the result like this:
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == MY_DATA_CHECK_CODE) {
        if (resultCode == TextToSpeech.Engine.CHECK_VOICE_DATA_PASS) {
            tts = new TextToSpeech(this, this);
        }
        else {
            // missing data, install it
            Intent installIntent = new Intent();
            installIntent.setAction(TextToSpeech.Engine.ACTION_INSTALL_TTS_DATA);
            startActivity(installIntent);
        }
    }
}
If the resultCode contains the value that you pass to the intent when you start the checking activity, then TTS is available.  If it's not, you can start another activity to ask the user if they want to install TTS data.

If TTS is available, then you can safely initialize TTS by create new TextToSpeech object.  You need to pass the class that implements OnInitListener as a parameter.  Class that implements OnInitListener must implement the function onInit(int status), which is called by TextToSpeech constructor to let the application knows whether initialize of TTS is successful.

public void onInit(int status) {
    if (status == TextToSpeech.SUCCESS) {
        TTS.setLanguage(Locale.US);
        imgTts = (ImageView) findViewById(R.id.imgTts);
        imgTts.setOnTouchListener(this);
        imgTts.setVisibility(View.VISIBLE);
    }
}
This is all you need to do to use TTS in your app.  So what's the problem?  If you look at the first step, where we need to start a new activity to check the availability of TTS, you will see that we can not put this step inside onResume!  Why, because starting new activity pauses the current activity.  Since you also release TTS in onPause(), you would release TTS as soon as you try to initialize TTS.  Not good. It's getting worse because when the checking activity is done, onResume() is called, resulted in your app trying to initialize TTS again, and it goes into an infinite loop of initialize/release the TTS.

You can try to fix the problem by having state variable to keep track of where the initialization is at, but for me, I just do what I saw in code on Android Development site (before it got faceliftted, I could not find it there anymore) and put initialize code in onCreate and release code in onDestroy.

Hope it helps.

No comments:

Post a Comment