Skip to content Skip to sidebar Skip to footer

Text To Speech Pattern In Android

I'm trying to use TextToSpeech in my app. I wrote it as a bound service. Problem appears when I need to read text before activity stops. It says just half of text because activity

Solution 1:

Instead of just bind service, start your service using startService() and then bind. After reading the text in onUtteranceCompleted() call stopSelf().

Solution 2:

Here is my implementation - as inspired by Hoan's answer (and added many years later but hopefully useful to someone else to save them having to work it out as I did.

import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.speech.tts.TextToSpeech;
import android.speech.tts.UtteranceProgressListener;

import java.util.LinkedList;
import java.util.Locale;
import java.util.Queue;

import timber.log.Timber;

/**
 * This is a simplified text to speech service where each "instance" (each time startService is
 * called) is only expected to speak once (we don't check this but the service gets stopped after it
 * has spoken once). This was created for the case where we want an announcement to be made just
 * before finishing an activity so couldn't do it in the activity itself because it gets destroyed
 * before it finishes speaking.
 *
 * <p>Usage (in the activity that "speaks"):
 * <li>Check there is text to speech data available on the device. See:
 *     https://android-developers.googleblog.com/2009/09/introduction-to-text-to-speech-in.html
 * <li>Call startService after successfully checking for speech data
 * <li>Bind the activity to the service onStart (and unbind onStop). Getting an instance of the service to later pass
 * the text to speak though to.
 * <li> call `.speak(textToSpeak)` when ready to speak (after which we cannot call `.speak()` again)
 *
 *     <p>NB: We assume that the activity that started this service checks that TTS data is
 *     available.
 *     <p>NB: You need to start the service a few seconds before asking it to speak as the text to speech api doesn't
 *     start immediately.
 */publicclassTTSSingleUtteranceServiceextendsService {
  privatebooleaninitDone=false;
  private TextToSpeech textToSpeech;
  Queue<Integer> startIds = newLinkedList<>();
  // Binder given to clientsprivatefinalIBinderbinder=newTTSBinder();

  /**
   * Class used for the client Binder. Because we know this service always runs in the same process
   * as its clients, we don't need to deal with IPC.
   */publicclassTTSBinderextendsBinder {
    public TTSSingleUtteranceService getService() {
      // Return this instance of TTSService so clients can call public methodsreturn TTSSingleUtteranceService.this;
    }
  }

  @OverridepublicvoidonCreate() {
    super.onCreate();
    // Initialise the TextToSpeech object
    textToSpeech =
        newTextToSpeech(
            this,
            status -> {
              if (status == TextToSpeech.SUCCESS) {
                initDone = true;
                Localelocale= Locale.getDefault();
                intttsLang= textToSpeech.setLanguage(locale);
                if (ttsLang == TextToSpeech.LANG_MISSING_DATA
                    || ttsLang == TextToSpeech.LANG_NOT_SUPPORTED) {
                  Timber.e("TTS: Language %s is not supported!", locale);
                }
                textToSpeech.setOnUtteranceProgressListener(
                    newUtteranceProgressListener() {
                      @OverridepublicvoidonStart(String utteranceId) {
                        Timber.i("Utterance started");
                      }

                      @OverridepublicvoidonDone(String utteranceId) {
                        Timber.i("Utterance completed. Shutting down service");
                        if (!startIds.isEmpty()) {
                          stopSelf(startIds.remove());
                        } else {
                          stopSelf();
                        }
                      }

                      @OverridepublicvoidonError(String utteranceId) {
                        Timber.i("Utterance errored");
                      }
                    });
                Timber.i("TTS: Initialization success with locale=%s.", locale);
              } else {
                Timber.e("Text to speech initialisation failed");
              }
            });
  }

  @OverridepublicintonStartCommand(Intent intent, int flags, int startId) {
    // add this startId to the queue of ids so that we can shutdown each one in order
    startIds.add(startId);
    // If we get killed, after returning from here, do not recreate the service unless there are// pending intents to deliver.return START_NOT_STICKY;
  }

  @Overridepublic IBinder onBind(Intent intent) {
    return binder;
  }

  @OverridepublicvoidonDestroy() {
    Timber.i("Destroying TTS service");
    if (textToSpeech != null) {
      textToSpeech.shutdown();
    }
    super.onDestroy();
  }

  /** method for clients */publicvoidspeak(String text) {
    Timber.i("speak called with text=%s", text);
    if (text != null && initDone) {
      StringutteranceId= String.valueOf(text.hashCode());
      textToSpeech.speak(text, TextToSpeech.QUEUE_ADD, null, utteranceId);
    } else {
      Timber.e("TTSError: textToSpeech is null or hasn't finished initialising!");
    }
  }
}

I then use this from the Android Activity like so

publicclassMyActivityextendsAppCompatActivity {
  TTSSingleUtteranceService ttsService;
  booleanboundToTtsService=false;

  @OverrideprotectedvoidonCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // Check there is text to speech data available on the device and then will start the text to// speech service in onActivityResultIntentcheckIntent=newIntent();
    checkIntent.setAction(TextToSpeech.Engine.ACTION_CHECK_TTS_DATA);
    startActivityForResult(checkIntent, TTS_CHECK_DATA_AVAILABLE);
  }

  @OverrideprotectedvoidonStart() {
    super.onStart();
    Intentintent=newIntent(this, TTSSingleUtteranceService.class);
    bindService(intent, connection, Context.BIND_AUTO_CREATE);
  }

  @OverrideprotectedvoidonStop() {
    super.onStop();
    unbindService(connection);
    boundToTtsService = false;
  }

  /** Defines callbacks for service binding, passed to bindService() */privateServiceConnectionconnection=newServiceConnection() {
        @OverridepublicvoidonServiceConnected(ComponentName className, IBinder service) {
          // We've bound to TTSService, cast the IBinder and get the TTSService instance
          TTSSingleUtteranceService.TTSBinderbinder= (TTSSingleUtteranceService.TTSBinder) service;
          ttsService = binder.getService();
          boundToTtsService = true;
        }

        @OverridepublicvoidonServiceDisconnected(ComponentName arg0) {
          boundToTtsService = false;
        }
      };

  publicvoidsaveClicked() {
    // this is called to save the data entered and then speak the results and finish the activity// perform needed logic// Speak textif (boundToTtsService) {
      ttsService.speak("This is the text that will be spoken");
    }
    finish();
  }

  @OverrideprotectedvoidonActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == TTS_CHECK_DATA_AVAILABLE) {
      if (resultCode == TextToSpeech.Engine.CHECK_VOICE_DATA_PASS) {
        // success, start the TTS service
        startService(newIntent(this, TTSSingleUtteranceService.class));
      } else {
        // missing data, install it
        Timber.i("TTS: Missing text to speech resources - redirecting user to install them.");
        IntentinstallIntent=newIntent();
        installIntent.setAction(TextToSpeech.Engine.ACTION_INSTALL_TTS_DATA);
        startActivity(installIntent);
      }
    }
  }
}

Finally don't forget to add the service to your AndroidManifest.xml (within the application section)

<serviceandroid:name=".util.TTSSingleUtteranceService" />

Post a Comment for "Text To Speech Pattern In Android"