net.bible.service.device.speak.TextToSpeechController.java Source code

Java tutorial

Introduction

Here is the source code for net.bible.service.device.speak.TextToSpeechController.java

Source

package net.bible.service.device.speak;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;

import net.bible.android.BibleApplication;
import net.bible.android.activity.R;
import net.bible.android.control.event.ABEventBus;
import net.bible.android.control.event.phonecall.PhoneCallMonitor;
import net.bible.android.control.event.phonecall.PhoneCallStarted;
import net.bible.android.view.activity.base.Dialogs;
import net.bible.service.common.CommonUtils;
import net.bible.service.device.speak.event.SpeakEvent;
import net.bible.service.device.speak.event.SpeakEvent.SpeakState;
import net.bible.service.device.speak.event.SpeakEventManager;

import org.apache.commons.lang.StringUtils;

import android.content.Context;
import android.speech.tts.TextToSpeech;
import android.util.Log;
import de.greenrobot.event.EventBus;

/**
 * <p>text-to-speech (TTS). Please note the following steps:</p>
 *
 * <ol>
 * <li>Construct the TextToSpeech object.</li>
 * <li>Handle initialization callback in the onInit method.
 * The activity implements TextToSpeech.OnInitListener for this purpose.</li>
 * <li>Call TextToSpeech.speak to synthesize speech.</li>
 * <li>Shutdown TextToSpeech in onDestroy.</li>
 * </ol>
 *
 * <p>Documentation:
 * http://developer.android.com/reference/android/speech/tts/package-summary.html
 * </p>
 * <ul>
 * @author Martin Denham [mjdenham at gmail dot com]
 * @see gnu.lgpl.License for license details.<br>
 *      The copyright to this program is held by it's author.
    
 */
public class TextToSpeechController
        implements TextToSpeech.OnInitListener, TextToSpeech.OnUtteranceCompletedListener {

    private static final String TAG = "Speak";

    private TextToSpeech mTts;

    private List<Locale> localePreferenceList;
    private Locale currentLocale = Locale.getDefault();
    private static String PERSIST_LOCALE_KEY = "SpeakLocale";

    private SpeakTextProvider mSpeakTextProvider;
    private SpeakTiming mSpeakTiming;

    private TTSLanguageSupport ttsLanguageSupport = new TTSLanguageSupport();

    private SpeakEventManager speakEventManager = SpeakEventManager.getInstance();

    private Context context;

    private static final String UTTERANCE_PREFIX = "AND-BIBLE-";
    private long uniqueUtteranceNo = 0;

    // tts.isSpeaking() returns false when multiple text is queued on some older versions of Android so maintain it manually
    private boolean isSpeaking = false;

    private boolean isPaused = false;

    private static final TextToSpeechController singleton = new TextToSpeechController();

    public static TextToSpeechController getInstance() {
        return singleton;
    }

    private TextToSpeechController() {
        Log.d(TAG, "Creating TextToSpeechController");
        context = BibleApplication.getApplication().getApplicationContext();
        mSpeakTextProvider = new SpeakTextProvider();
        mSpeakTiming = new SpeakTiming();
        restorePauseState();
    }

    public boolean isLanguageAvailable(String langCode) {
        return ttsLanguageSupport.isLangKnownToBeSupported(langCode);
    }

    public synchronized void speak(List<Locale> localePreferenceList, List<String> textToSpeak, boolean queue) {
        Log.d(TAG, "speak strings" + (queue ? " queued" : ""));
        if (!queue) {
            Log.d(TAG, "Queue is false so requesting stop");
            clearTtsQueue();
        } else if (isPaused()) {
            Log.d(TAG, "New speak request while paused so clearing paused speech");
            clearTtsQueue();
            isPaused = false;
        }
        mSpeakTextProvider.addTextsToSpeak(textToSpeak);

        // currently can't change Locale until speech ends
        this.localePreferenceList = localePreferenceList;

        startSpeakingInitingIfRequired();
    }

    private void startSpeakingInitingIfRequired() {
        if (mTts == null) {
            Log.d(TAG, "mTts was null so initialising Tts");

            try {
                // Initialize text-to-speech. This is an asynchronous operation.
                // The OnInitListener (second argument) is called after initialization completes.
                mTts = new TextToSpeech(context, this // TextToSpeech.OnInitListener
                );
            } catch (Exception e) {
                Log.e(TAG, "Error initialising Tts", e);
                showError(R.string.error_occurred);
            }
        } else {
            startSpeaking();
        }
    }

    // Implements TextToSpeech.OnInitListener.
    @Override
    public void onInit(int status) {
        Log.d(TAG, "Tts initialised");
        boolean isOk = false;

        // status can be either TextToSpeech.SUCCESS or TextToSpeech.ERROR.
        if (mTts != null && status == TextToSpeech.SUCCESS) {
            Log.d(TAG, "Tts initialisation succeeded");
            boolean localeOK = false;
            Locale locale = null;
            for (int i = 0; i < localePreferenceList.size() && !localeOK; i++) {
                locale = localePreferenceList.get(i);
                Log.d(TAG, "Checking for locale:" + locale);
                int result = mTts.setLanguage(locale);
                localeOK = ((result != TextToSpeech.LANG_MISSING_DATA)
                        && (result != TextToSpeech.LANG_NOT_SUPPORTED));
                if (localeOK) {
                    Log.d(TAG, "Successful locale:" + locale);
                    currentLocale = locale;
                }
            }

            if (!localeOK) {
                Log.e(TAG, "TTS missing or not supported");
                // Language data is missing or the language is not supported.
                ttsLanguageSupport.addUnsupportedLocale(locale);
                showError(R.string.tts_lang_not_available);
            } else {
                // The TTS engine has been successfully initialized.
                ttsLanguageSupport.addSupportedLocale(locale);
                int ok = mTts.setOnUtteranceCompletedListener(this);
                if (ok == TextToSpeech.ERROR) {
                    Log.e(TAG, "Error registering onUtteranceCompletedListener");
                } else {
                    // everything seems to have succeeded if we get here
                    isOk = true;
                    // say the text
                    startSpeaking();

                    // add event listener to stop on call
                    stopIfPhoneCall();
                }
            }
        } else {
            Log.d(TAG, "Tts initialisation failed");
            // Initialization failed.
            showError(R.string.error_occurred);
        }

        if (!isOk) {
            shutdown();
        }
    }

    /**
     * Add event listener to stop on call
     */
    protected void stopIfPhoneCall() {

        PhoneCallMonitor.ensureMonitoringStarted();

        // listen for phone call in order to pause speak
        ABEventBus.getDefault().safelyRegister(this);
    }

    public synchronized void rewind() {
        Log.d(TAG, "Rewind TTS");
        // prevent onUtteranceCompleted causing next text to be grabbed
        uniqueUtteranceNo++;
        boolean wasPaused = isPaused;
        isPaused = true;
        if (isSpeaking) {
            mTts.stop();
        }
        isSpeaking = false;

        if (!wasPaused) {
            // ensure current position is saved which is done during pause
            mSpeakTextProvider.pause(mSpeakTiming.getFractionCompleted());
        }

        // move current position back a bit
        mSpeakTextProvider.rewind();

        isPaused = wasPaused;
        if (!isPaused) {
            startSpeakingInitingIfRequired();
        }
    }

    public synchronized void forward() {
        Log.d(TAG, "Forward TTS");
        // prevent onUtteranceCompleted causing next text to be grabbed
        uniqueUtteranceNo++;
        boolean wasPaused = isPaused;
        isPaused = true;
        if (isSpeaking) {
            mTts.stop();
        }
        isSpeaking = false;

        if (!wasPaused) {
            // ensure current position is saved which is done during pause
            mSpeakTextProvider.pause(mSpeakTiming.getFractionCompleted());
        }

        // move current position back a bit
        mSpeakTextProvider.forward();

        isPaused = wasPaused;
        if (!isPaused) {
            startSpeakingInitingIfRequired();
        }
    }

    public synchronized void pause() {
        Log.d(TAG, "Pause TTS");

        if (isSpeaking()) {
            isPaused = true;
            isSpeaking = false;

            mSpeakTextProvider.pause(mSpeakTiming.getFractionCompleted());

            //kill the tts engine because it could be a long ime before restart and the engine may become corrupted or used elsewhere
            shutdownTtsEngine();

            fireStateChangeEvent();
        }
    }

    public synchronized void continueAfterPause() {
        try {
            Log.d(TAG, "continue after pause");
            isPaused = false;

            // ask TTs to say the text
            startSpeakingInitingIfRequired();
        } catch (Exception e) {
            Log.e(TAG, "TTS Error continuing after Pause", e);
            mSpeakTextProvider.reset();
            isSpeaking = false;
            shutdown();
        }

        // should be able to clear this because we are now speaking
        isPaused = false;
    }

    /** only check timing when paused to prevent concurrency problems
     */
    public long getPausedTotalSeconds() {
        return mSpeakTiming.getSecsForChars(mSpeakTextProvider.getTotalChars());
    }

    public long getPausedCompletedSeconds() {
        return mSpeakTiming.getSecsForChars(mSpeakTextProvider.getSpokenChars());
    }

    private void startSpeaking() {
        Log.d(TAG, "about to send all text to TTS");
        // ask TTs to say the text
        if (!isSpeaking) {
            speakNextChunk();
            isSpeaking = true;
            isPaused = false;
            fireStateChangeEvent();
        }

        // should be able to clear this because we are now speaking
        isPaused = false;
    }

    @Override
    public void onUtteranceCompleted(String utteranceId) {
        Log.d(TAG, "onUtteranceCompleted:" + utteranceId);
        // pause/rew/ff can sometimes allow old messages to complete so need to prevent move to next sentence if completed utterance is out of date 
        if ((!isPaused && isSpeaking) && StringUtils.startsWith(utteranceId, UTTERANCE_PREFIX)) {
            long utteranceNo = Long.valueOf(StringUtils.removeStart(utteranceId, UTTERANCE_PREFIX));
            if (utteranceNo == uniqueUtteranceNo - 1) {
                mSpeakTextProvider.finishedUtterance(utteranceId);

                // estimate cps
                mSpeakTiming.finished(utteranceId);

                // ask TTs to say the text
                if (mSpeakTextProvider.isMoreTextToSpeak()) {
                    speakNextChunk();
                } else {
                    Log.d(TAG, "Shutting down TTS");
                    shutdown();
                }
            }
        }
    }

    private void speakNextChunk() {
        String text = mSpeakTextProvider.getNextTextToSpeak();
        if (text.length() > 0) {
            speakString(text);
        }
    }

    private void speakString(String text) {
        if (mTts == null) {
            Log.e(TAG, "Error: attempt to speak when tts is null.  Text:" + text);
        } else {
            // Always set the UtteranceId (or else OnUtteranceCompleted will not be called)
            HashMap<String, String> dummyTTSParams = new HashMap<String, String>();
            String utteranceId = UTTERANCE_PREFIX + uniqueUtteranceNo++;
            dummyTTSParams.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, utteranceId);

            Log.d(TAG, "do speak substring of length:" + text.length() + " utteranceId:" + utteranceId);
            mTts.speak(text, TextToSpeech.QUEUE_ADD, // handle flush by clearing text queue 
                    dummyTTSParams);

            mSpeakTiming.started(utteranceId, text.length());
            isSpeaking = true;
            //           Log.d(TAG, "Speaking:"+text);
        }
    }

    /** flush cached text
     */
    private void clearTtsQueue() {
        Log.d(TAG, "Stop TTS");

        // Don't forget to shutdown!
        if (isSpeaking()) {
            Log.d(TAG, "Flushing speech");
            // flush remaining text
            mTts.speak(" ", TextToSpeech.QUEUE_FLUSH, null);
        }

        mSpeakTextProvider.reset();
        isSpeaking = false;
    }

    private void showError(int msgId) {
        Dialogs.getInstance().showErrorMsg(msgId);
    }

    public void shutdown() {
        Log.d(TAG, "Shutdown TTS");

        isSpeaking = false;
        isPaused = false;

        // tts.stop can trigger onUtteranceCompleted so set above flags first to avoid sending of a further text and setting isSpeaking to true
        shutdownTtsEngine();
        mSpeakTextProvider.reset();

        fireStateChangeEvent();
    }

    private void shutdownTtsEngine() {
        Log.d(TAG, "Shutdown TTS Engine");
        try {
            // Don't forget to shutdown!
            if (mTts != null) {
                try {
                    mTts.stop();
                } catch (Exception e) {
                    Log.e(TAG, "Error stopping Tts engine", e);
                }
                mTts.shutdown();

                // de-register from EventBus
                EventBus.getDefault().unregister(this);
            }
        } catch (Exception e) {
            Log.e(TAG, "Error shutting down Tts engine", e);
        } finally {
            mTts = null;
        }
    }

    private void fireStateChangeEvent() {
        if (isPaused) {
            speakEventManager.speakStateChanged(new SpeakEvent(SpeakState.PAUSED));
        } else if (isSpeaking) {
            speakEventManager.speakStateChanged(new SpeakEvent(SpeakState.SPEAKING));
        } else {
            speakEventManager.speakStateChanged(new SpeakEvent(SpeakState.SILENT));
        }

    }

    public boolean isSpeaking() {
        return isSpeaking;
    }

    public boolean isPaused() {
        return isPaused;
    }

    /**
     * Pause speak if phone call starts
     */
    public void onEvent(PhoneCallStarted event) {
        if (isSpeaking()) {
            pause();
        }

        if (isPaused()) {
            persistPauseState();
        } else {
            // ensure a previous pause does not hang around and be restored incorrectly
            clearPauseState();
        }

        shutdownTtsEngine();
    }

    /** persist and restore pause state to allow pauses to continue over an app exit
     */
    private void persistPauseState() {
        Log.d(TAG, "Persisting Pause state");
        mSpeakTextProvider.persistState();
        CommonUtils.getSharedPreferences().edit().putString(PERSIST_LOCALE_KEY, currentLocale.toString()).commit();
    }

    private void restorePauseState() {
        // ensure no relevant current state is overwritten accidentally
        if (!isSpeaking() && !isPaused()) {
            Log.d(TAG, "Attempting to restore any Persisted Pause state");
            isPaused = mSpeakTextProvider.restoreState();

            // restore locale information so tts knows which voice to load when it initialises
            currentLocale = new Locale(CommonUtils.getSharedPreferences().getString(PERSIST_LOCALE_KEY,
                    Locale.getDefault().toString()));
            localePreferenceList = new ArrayList<Locale>();
            localePreferenceList.add(currentLocale);
        }
    }

    private void clearPauseState() {
        Log.d(TAG, "Clearing Persisted Pause state");
        mSpeakTextProvider.clearPersistedState();
        CommonUtils.getSharedPreferences().edit().remove(PERSIST_LOCALE_KEY).commit();
    }
}