真・喋るドーナツ

最初にお詫びです。昨日の結論は間違っていました。
TTSを用いるのに、onInitの中でのみ利用できるということはありませんでした。
TTSが初期化されるのには十分に長い時間が必要ですので、onCreateやonStartのような最初のほうで実行されるようなメソッドの中ではTTSの初期化が終わらないということだと思います。またメインスレッドから切り替わらないと初期化が行われないのかもしれません。
詳細はわかりませんんが、遅めのメソッド、例えばActivity#onWindowFocusChangedにてonStartにて作成したTTSのインスタンスを利用したら使用できました。


ここからTTSのインスタンスを利用するには確実な方法はonInitの中での実行、そうでなければActivityに状態変数としてフラグを作成し、onInitでフラグを立ててonClickの中などでフラグを見て実行する感じでしょうか。
AsyncTaskを用いてフラグが立つまでActivityの全てをダイアログを用いてロード中のように利用できないようにしてしまっても良いかもしれません。

ここから

連休も終わりうだうだネットを見ていたところ丁度タイミング悪く公式ブログにてTextToSpeechの詳細が公表されました。

Android Developers Blog: An introduction to Text-To-Speech in Android

もう一日早かったら昨日のバカ記事はなかったんです。
やはりSDKの新機能は見です。

でもせっかく勉強を始めましたので公式ブログの発表内容を簡単にまとめてみます。
以下、TextToSpeechはTTSと略です。

TextToSpeechへのイントロダクション

TTSのサポート言語は次の5つです。


あれ?昨日より一つ増えていますね?
昨日はLocaleに定義されている定数を全部試しましたが、スペインはLocaleの定数に入っていないんですね。


スピーチエンジンは設定言語に従って単語を異なる発音で喋ることができます。
例として"Paris"という単語は英語と仏語で違う発音となります。
スピーチエンジンは話をさせる前にその言語のリソースを読み込む必要があります。


スピーチエンジンを搭載するAndroidバイスは言語データを保持しているとは限りません。
そのためプログラマはまず実行端末上に言語データが存在するかどうかチェックを行わなければなりません。

Intent checkIntent = new Intent();
checkIntent.setAction(TextToSpeech.Engine.ACTION_CHECK_TTS_DATA);
startActivityForResult(checkIntent, MY_DATA_CHECK_CODE);

おぉ、Intentでチェックしてますね。
さすが本家は一味違います。
MY_DATA_CEHCK_CODEは任意のint値ですね。startActivityResultでこの任意の数値を渡すと子のActivityが終了したときこのコードの値が何の呼出の返り値かの判定としてonActivityResultにて使われます。


さて、言語データが既にデバイス上に存在すればCHECK_VOICE_DATA_PASSが返ってきます。

    protected void onActivityResult(
            int requestCode, int resultCode, Intent data) {
        if (requestCode == MY_DATA_CHECK_CODE) {
            if (resultCode == TextToSpeech.Engine.CHECK_VOICE_DATA_PASS) {
                // success, create the TTS instance
                mTts = new TextToSpeech(this, this);
            } else {
                // missing data, install it
                Intent installIntent = new Intent();
                installIntent.setAction(
                    TextToSpeech.Engine.ACTION_INSTALL_TTS_DATA);
                startActivity(installIntent);
            }
        }
    }

ここで注目はelse文の中です。
もしTTSのリソースがない場合には再び別のIntentを投げてリソースをダウンロードするようにユーザーに促すそうです。
さて、問題です。
私もあなたも1.6対応端末はエミュレータしかありません。
そのエミュレータで上のIntentを投げてみたところエラーで落ちました(笑

09-25 00:04:24.439: ERROR/AndroidRuntime(701): Uncaught handler: thread main exiting due to uncaught exception
09-25 00:04:24.461: ERROR/AndroidRuntime(701): java.lang.RuntimeException: Unable to start activity ComponentInfo{com.svox.pico/com.svox.pico.DownloadVoiceData}: android.content.ActivityNotFoundException: No Activity found to handle Intent { act=android.intent.action.VIEW dat=market://search?q=pname:com.svox.langpack.installer }
09-25 00:04:24.461: ERROR/AndroidRuntime(701):     at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2401)
09-25 00:04:24.461: ERROR/AndroidRuntime(701):     at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2417)
09-25 00:04:24.461: ERROR/AndroidRuntime(701):     at android.app.ActivityThread.access$2100(ActivityThread.java:116)
09-25 00:04:24.461: ERROR/AndroidRuntime(701):     at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1794)
09-25 00:04:24.461: ERROR/AndroidRuntime(701):     at android.os.Handler.dispatchMessage(Handler.java:99)
09-25 00:04:24.461: ERROR/AndroidRuntime(701):     at android.os.Looper.loop(Looper.java:123)
09-25 00:04:24.461: ERROR/AndroidRuntime(701):     at android.app.ActivityThread.main(ActivityThread.java:4203)
09-25 00:04:24.461: ERROR/AndroidRuntime(701):     at java.lang.reflect.Method.invokeNative(Native Method)
09-25 00:04:24.461: ERROR/AndroidRuntime(701):     at java.lang.reflect.Method.invoke(Method.java:521)
09-25 00:04:24.461: ERROR/AndroidRuntime(701):     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:791)
09-25 00:04:24.461: ERROR/AndroidRuntime(701):     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:549)
09-25 00:04:24.461: ERROR/AndroidRuntime(701):     at dalvik.system.NativeStart.main(Native Method)
09-25 00:04:24.461: ERROR/AndroidRuntime(701): Caused by: android.content.ActivityNotFoundException: No Activity found to handle Intent { act=android.intent.action.VIEW dat=market://search?q=pname:com.svox.langpack.installer }
09-25 00:04:24.461: ERROR/AndroidRuntime(701):     at android.app.Instrumentation.checkStartActivityResult(Instrumentation.java:1484)
09-25 00:04:24.461: ERROR/AndroidRuntime(701):     at android.app.Instrumentation.execStartActivity(Instrumentation.java:1454)
09-25 00:04:24.461: ERROR/AndroidRuntime(701):     at android.app.Activity.startActivityForResult(Activity.java:2660)
09-25 00:04:24.461: ERROR/AndroidRuntime(701):     at com.svox.pico.DownloadVoiceData.onCreate(DownloadVoiceData.java:34)
09-25 00:04:24.461: ERROR/AndroidRuntime(701):     at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1123)
09-25 00:04:24.461: ERROR/AndroidRuntime(701):     at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2364)
09-25 00:04:24.461: ERROR/AndroidRuntime(701):     ... 11 more

ActivityNotFoundExceptionですね。
実装されてませんね。
なめてますね。


さて気をとりなおして、言語リソースがあった場合上のコードではいきなりTTSのインスタンスを取得しています。
ここでonInitの中で喋らせるのか、その後、のコードがないのでわかりにくいですね。
今回の公式BlogはonInitで何をすべきか、の記述がないのがとても残念です。



さてGoogle I/OでTTSの解説は行われていたんですね。解説のビデオが録画されています。
http://www.youtube.com/watch?v=uX9nt8Cpdqg#t=6m17s

見たところ新機能紹介にすぎない内容でしたので使用する上で観る必要はないと思います。
でも、その後のジェスチャーの紹介がやっぱり魅力的ですね。
早くこっちの使い方を紹介して。


さて、馬鹿は放っておいて進みます。
onInitの中で喋らせるにはまず言語を指定します。

mTts.setLanguage(Locale.US);


上の方法で言語を設定する前に目的言語が使用可能かチェックせねばなりません。
これは昨日と同じですね。

mTts.isLanguageAvailable(Locale.UK))
mTts.isLanguageAvailable(Locale.FRANCE))
mTts.isLanguageAvailable(new Locale("spa", "ESP")))

この指定方法ですと使用可能であればTextToSpeech.LANG_COUNTRY_AVAILABLEが返されその国の国語が使用可能であることを示します。

mTts.isLanguageAvailable(Locale.CANADA_FRENCH))
mTts.isLanguageAvailable(new Locale("spa"))

この指定方法ですとフランス語は存在しますが、カナダ方言はないとか、スペイン語のみの指定ですので国の情報が足りないという意味でTextToSpeech.LANG_AVAILABLEが返ります。
どちらも使用不可能な言語が指定された場合にはTextToSpeech.LANG_MISSING_DATAが返ります。



指定した言語と喋りの指定に与えた文字列の基づく言語が異なる場合、「面白い」結果となります。
ユーザーは何が起こったのか理解できないので必ず言語指定が間違いないようにしましょう。
特にユーザーのデフォルトロケールのまま処理させるような場合には最低でもデフォルトロケールが使用可能か判断しましょう、とのこと。

あなたのアプリを喋らせましょう

最も簡単に喋らせる方法はspeakメソッドの利用です。

String myText1 = "Did you sleep well?";
String myText2 = "I hope so, because it's time to wake up.";
mTts.speak(myText1, TextToSpeech.QUEUE_FLUSH, null);
mTts.speak(myText2, TextToSpeech.QUEUE_ADD, null);

TTSのインスタンスはそれぞれのキューを持ちます。
キューに突込むセリフを"utterance"と呼びます。珍しい単語ですが発話とか言葉とかそのまんまの意味のようです。
一つ目のspeakの呼出ではキューをフラッシュして(つまり空にして)先頭にmyText1を入れます。
つまりmyText1がすぐ話されます。
二つ目のspeakではqueueに追加されます。
speakメソッドは非同期なので実行するとすぐ終了します。

次にパラメータの利用方法です。

HashMap<String, String> myHashAlarm = new HashMap();
myHashAlarm.put(TextToSpeech.Engine.KEY_PARAM_STREAM,
        String.valueOf(AudioManager.STREAM_ALARM));
mTts.speak(myText1, TextToSpeech.QUEUE_FLUSH, myHashAlarm);
mTts.speak(myText2, TextToSpeech.QUEUE_ADD, myHashAlarm);

上は公式blogの原文のままですが、Eclipseに突込むと怒られます。
HashMapで宣言していますのでコンストラクタもnew HashMap()にしないと駄目ですね。JDK7では省略形が利用できてnew HashMap<>()だけで良いそうです。
AndroidってJDK5のまんまなのでしょうか。メモリがつらいとかあるでしょうけどどんどん新しいJDKに対応してほしいですね!


さて、上の例ではspeakにALARMのストリームを利用します。
Androidでは複数のオーディオストリームが存在し、それらはAudioManagerにて管理されているそうです。
ここは詳しく知らないのでパス。
トーキングアラームクロックを作りたい場合などに上記のように書くようです。


どうも単純に実行するだけだと鳴りませんでした。

発話の終了を検知する

ドーナツに喋らせる場合にキューに突っ込んでサービスが喋ります。
speakメソッドは非同期ですので発話の終了がわかりません。
それを知るためにリスナーのTextToSpeech.OnUtteranceCompletedListenerを設定します。

mTts.setOnUtteranceCompletedListener(this);
myHashAlarm.put(TextToSpeech.Engine.KEY_PARAM_STREAM,
        String.valueOf(AudioManager.STREAM_ALARM));
mTts.speak(myText1, TextToSpeech.QUEUE_FLUSH, myHashAlarm);
myHashAlarm.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID,
        "end of wakeup message ID");
// myHashAlarm now contains two optional parameters
mTts.speak(myText2, TextToSpeech.QUEUE_ADD, myHashAlarm)

上の例ではまたActivity自身をリスナーとして設定しています。
という訳でActivityにimplementsを加え、以下のメソッドを追加します。

public void onUtteranceCompleted(String uttId) {
    if (uttId == "end of wakeup message ID") {
        playAnnoyingMusic();
    } 
}


この例もおかしいですね。文字列定数とはいえ"=="を使うのは危険です。String#equalsを使うべきではないでしょうか?
この例ですと発話が終了したらplayAnnoyingMusic()を呼びだします。詳細は不明です。

ファイルへの記録とプレイバック

さてspeakで喋らせるのは重いです。最初の最初の実行には結構なラグが発生します。
TTSには発話内容をサウンドファイルとして保存し後からファイルから再生する便利な機能があります。

HashMap<String, String> myHashRender = new HashMap();
String wakeUpText = "Are you up yet?";
String destFileName = "/sdcard/myAppCache/wakeUp.wav";
myHashRender.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, wakeUpText);
mTts.synthesizeToFile(wakuUpText, myHashRender, destFileName);

この例の面白いのは発話の識別IDがそのまま発話内容のテキストになっていることですね。
destFileNameに作成されたファイルはそのままMediaPlayerにて再生できるそうです。
ファイルのパスを直書きしていますが、SDCARDについてはEnvironment.getExternalStorageDirectory()を用いたほうが将来的には良いでしょう。


さてMediaPlayerでも実行できるのですが、再生用のメソッドもあります。

mTts.addSpeech(wakeUpText, destFileName);

これによりその後のspeak()の実行はdestFileNameの再生となります。
もしファイルが無くなった場合には普通に発話が再実行されます。
もちろんHashMapでのパラメータの利用もファイルの出自に関係なく有効になります。

TTSを利用しなくなったら

TTSを利用しなくなったらshutdown()してください。
onDestroyにてコールすると良いでしょう。