TtsService.java revision 6a0e293c84de02e819c1b402141bd3f7684ea164
1/*
2 * Copyright (C) 2009 Google Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16package android.tts;
17
18import android.app.Service;
19import android.content.ContentResolver;
20import android.content.Context;
21import android.content.Intent;
22import android.content.SharedPreferences;
23import android.content.pm.PackageManager;
24import android.content.pm.PackageManager.NameNotFoundException;
25import android.media.MediaPlayer;
26import android.media.MediaPlayer.OnCompletionListener;
27import android.net.Uri;
28import android.os.IBinder;
29import android.os.RemoteCallbackList;
30import android.os.RemoteException;
31import android.preference.PreferenceManager;
32import android.speech.tts.ITts.Stub;
33import android.speech.tts.ITtsCallback;
34import android.speech.tts.TextToSpeech;
35import android.util.Log;
36import java.util.ArrayList;
37import java.util.Arrays;
38import java.util.HashMap;
39import java.util.concurrent.locks.ReentrantLock;
40
41/**
42 * @hide Synthesizes speech from text. This is implemented as a service so that
43 *       other applications can call the TTS without needing to bundle the TTS
44 *       in the build.
45 *
46 */
47public class TtsService extends Service implements OnCompletionListener {
48
49    private static class SpeechItem {
50        public static final int SPEECH = 0;
51        public static final int EARCON = 1;
52        public static final int SILENCE = 2;
53        public String mText = null;
54        public ArrayList<String> mParams = null;
55        public int mType = SPEECH;
56        public long mDuration = 0;
57
58        public SpeechItem(String text, ArrayList<String> params, int itemType) {
59            mText = text;
60            mParams = params;
61            mType = itemType;
62        }
63
64        public SpeechItem(long silenceTime) {
65            mDuration = silenceTime;
66        }
67    }
68
69    /**
70     * Contains the information needed to access a sound resource; the name of
71     * the package that contains the resource and the resID of the resource
72     * within that package.
73     */
74    private static class SoundResource {
75        public String mSourcePackageName = null;
76        public int mResId = -1;
77        public String mFilename = null;
78
79        public SoundResource(String packageName, int id) {
80            mSourcePackageName = packageName;
81            mResId = id;
82            mFilename = null;
83        }
84
85        public SoundResource(String file) {
86            mSourcePackageName = null;
87            mResId = -1;
88            mFilename = file;
89        }
90    }
91
92    private static final String ACTION = "android.intent.action.USE_TTS";
93    private static final String CATEGORY = "android.intent.category.TTS";
94    private static final String PKGNAME = "android.tts";
95
96    final RemoteCallbackList<android.speech.tts.ITtsCallback> mCallbacks = new RemoteCallbackList<ITtsCallback>();
97
98    private Boolean mIsSpeaking;
99    private ArrayList<SpeechItem> mSpeechQueue;
100    private HashMap<String, SoundResource> mEarcons;
101    private HashMap<String, SoundResource> mUtterances;
102    private MediaPlayer mPlayer;
103    private TtsService mSelf;
104
105    private ContentResolver mResolver;
106
107    private final ReentrantLock speechQueueLock = new ReentrantLock();
108    private final ReentrantLock synthesizerLock = new ReentrantLock();
109
110    private SynthProxy nativeSynth;
111
112    @Override
113    public void onCreate() {
114        super.onCreate();
115        Log.i("TTS", "TTS starting");
116
117        mResolver = getContentResolver();
118
119        String soLibPath = "/system/lib/libttspico.so";
120        nativeSynth = new SynthProxy(soLibPath);
121
122        mSelf = this;
123        mIsSpeaking = false;
124
125        mEarcons = new HashMap<String, SoundResource>();
126        mUtterances = new HashMap<String, SoundResource>();
127
128        mSpeechQueue = new ArrayList<SpeechItem>();
129        mPlayer = null;
130
131        setDefaultSettings();
132    }
133
134    @Override
135    public void onDestroy() {
136        super.onDestroy();
137        // Don't hog the media player
138        cleanUpPlayer();
139
140        nativeSynth.shutdown();
141
142        // Unregister all callbacks.
143        mCallbacks.kill();
144    }
145
146
147    private void setDefaultSettings() {
148
149        // TODO handle default language
150        setLanguage("eng", "USA", "");
151
152        // speech rate
153        setSpeechRate(getDefaultRate());
154
155    }
156
157
158    private boolean isDefaultEnforced() {
159        return (android.provider.Settings.Secure.getInt(mResolver,
160                    android.provider.Settings.Secure.TTS_USE_DEFAULTS,
161                    TextToSpeech.Engine.FALLBACK_TTS_USE_DEFAULTS)
162                == 1 );
163    }
164
165
166    private int getDefaultRate() {
167        return android.provider.Settings.Secure.getInt(mResolver,
168                android.provider.Settings.Secure.TTS_DEFAULT_RATE,
169                TextToSpeech.Engine.FALLBACK_TTS_DEFAULT_RATE);
170    }
171
172
173    private String getDefaultLanguage() {
174        String defaultLang = android.provider.Settings.Secure.getString(mResolver,
175                android.provider.Settings.Secure.TTS_DEFAULT_LANG);
176        if (defaultLang == null) {
177            return TextToSpeech.Engine.FALLBACK_TTS_DEFAULT_LANG;
178        } else {
179            return defaultLang;
180        }
181    }
182
183
184    private String getDefaultCountry() {
185        String defaultCountry = android.provider.Settings.Secure.getString(mResolver,
186                android.provider.Settings.Secure.TTS_DEFAULT_COUNTRY);
187        if (defaultCountry == null) {
188            return TextToSpeech.Engine.FALLBACK_TTS_DEFAULT_COUNTRY;
189        } else {
190            return defaultCountry;
191        }
192    }
193
194
195    private String getDefaultLocVariant() {
196        String defaultVar = android.provider.Settings.Secure.getString(mResolver,
197                android.provider.Settings.Secure.TTS_DEFAULT_VARIANT);
198        if (defaultVar == null) {
199            return TextToSpeech.Engine.FALLBACK_TTS_DEFAULT_VARIANT;
200        } else {
201            return defaultVar;
202        }
203    }
204
205
206    private void setSpeechRate(int rate) {
207        if (isDefaultEnforced()) {
208            nativeSynth.setSpeechRate(getDefaultRate());
209        } else {
210            nativeSynth.setSpeechRate(rate);
211        }
212    }
213
214
215    private void setPitch(int pitch) {
216        nativeSynth.setPitch(pitch);
217    }
218
219
220    private void setLanguage(String lang, String country, String variant) {
221        Log.v("TTS", "TtsService.setLanguage(" + lang + ", " + country + ", " + variant + ")");
222        if (isDefaultEnforced()) {
223            nativeSynth.setLanguage(getDefaultLanguage(), getDefaultCountry(),
224                    getDefaultLocVariant());
225        } else {
226            nativeSynth.setLanguage(lang, country, variant);
227        }
228    }
229
230
231    /**
232     * Adds a sound resource to the TTS.
233     *
234     * @param text
235     *            The text that should be associated with the sound resource
236     * @param packageName
237     *            The name of the package which has the sound resource
238     * @param resId
239     *            The resource ID of the sound within its package
240     */
241    private void addSpeech(String text, String packageName, int resId) {
242        mUtterances.put(text, new SoundResource(packageName, resId));
243    }
244
245    /**
246     * Adds a sound resource to the TTS.
247     *
248     * @param text
249     *            The text that should be associated with the sound resource
250     * @param filename
251     *            The filename of the sound resource. This must be a complete
252     *            path like: (/sdcard/mysounds/mysoundbite.mp3).
253     */
254    private void addSpeech(String text, String filename) {
255        mUtterances.put(text, new SoundResource(filename));
256    }
257
258    /**
259     * Adds a sound resource to the TTS as an earcon.
260     *
261     * @param earcon
262     *            The text that should be associated with the sound resource
263     * @param packageName
264     *            The name of the package which has the sound resource
265     * @param resId
266     *            The resource ID of the sound within its package
267     */
268    private void addEarcon(String earcon, String packageName, int resId) {
269        mEarcons.put(earcon, new SoundResource(packageName, resId));
270    }
271
272    /**
273     * Adds a sound resource to the TTS as an earcon.
274     *
275     * @param earcon
276     *            The text that should be associated with the sound resource
277     * @param filename
278     *            The filename of the sound resource. This must be a complete
279     *            path like: (/sdcard/mysounds/mysoundbite.mp3).
280     */
281    private void addEarcon(String earcon, String filename) {
282        mEarcons.put(earcon, new SoundResource(filename));
283    }
284
285    /**
286     * Speaks the given text using the specified queueing mode and parameters.
287     *
288     * @param text
289     *            The text that should be spoken
290     * @param queueMode
291     *            0 for no queue (interrupts all previous utterances), 1 for
292     *            queued
293     * @param params
294     *            An ArrayList of parameters. This is not implemented for all
295     *            engines.
296     */
297    private void speak(String text, int queueMode, ArrayList<String> params) {
298        if (queueMode == 0) {
299            stop();
300        }
301        mSpeechQueue.add(new SpeechItem(text, params, SpeechItem.SPEECH));
302        if (!mIsSpeaking) {
303            processSpeechQueue();
304        }
305    }
306
307    /**
308     * Plays the earcon using the specified queueing mode and parameters.
309     *
310     * @param earcon
311     *            The earcon that should be played
312     * @param queueMode
313     *            0 for no queue (interrupts all previous utterances), 1 for
314     *            queued
315     * @param params
316     *            An ArrayList of parameters. This is not implemented for all
317     *            engines.
318     */
319    private void playEarcon(String earcon, int queueMode,
320            ArrayList<String> params) {
321        if (queueMode == 0) {
322            stop();
323        }
324        mSpeechQueue.add(new SpeechItem(earcon, params, SpeechItem.EARCON));
325        if (!mIsSpeaking) {
326            processSpeechQueue();
327        }
328    }
329
330    /**
331     * Stops all speech output and removes any utterances still in the queue.
332     */
333    private void stop() {
334        Log.i("TTS", "Stopping");
335        mSpeechQueue.clear();
336
337        nativeSynth.stop();
338        mIsSpeaking = false;
339        if (mPlayer != null) {
340            try {
341                mPlayer.stop();
342            } catch (IllegalStateException e) {
343                // Do nothing, the player is already stopped.
344            }
345        }
346        Log.i("TTS", "Stopped");
347    }
348
349    public void onCompletion(MediaPlayer arg0) {
350        processSpeechQueue();
351    }
352
353    private void playSilence(long duration, int queueMode,
354            ArrayList<String> params) {
355        if (queueMode == 0) {
356            stop();
357        }
358        mSpeechQueue.add(new SpeechItem(duration));
359        if (!mIsSpeaking) {
360            processSpeechQueue();
361        }
362    }
363
364    private void silence(final long duration) {
365        class SilenceThread implements Runnable {
366            public void run() {
367                try {
368                    Thread.sleep(duration);
369                } catch (InterruptedException e) {
370                    e.printStackTrace();
371                } finally {
372                    processSpeechQueue();
373                }
374            }
375        }
376        Thread slnc = (new Thread(new SilenceThread()));
377        slnc.setPriority(Thread.MIN_PRIORITY);
378        slnc.start();
379    }
380
381    private void speakInternalOnly(final String text,
382            final ArrayList<String> params) {
383        class SynthThread implements Runnable {
384            public void run() {
385                boolean synthAvailable = false;
386                try {
387                    synthAvailable = synthesizerLock.tryLock();
388                    if (!synthAvailable) {
389                        Thread.sleep(100);
390                        Thread synth = (new Thread(new SynthThread()));
391                        synth.setPriority(Thread.MIN_PRIORITY);
392                        synth.start();
393                        return;
394                    }
395                    nativeSynth.speak(text);
396                } catch (InterruptedException e) {
397                    e.printStackTrace();
398                } finally {
399                    // This check is needed because finally will always run;
400                    // even if the
401                    // method returns somewhere in the try block.
402                    if (synthAvailable) {
403                        synthesizerLock.unlock();
404                    }
405                }
406            }
407        }
408        Thread synth = (new Thread(new SynthThread()));
409        synth.setPriority(Thread.MIN_PRIORITY);
410        synth.start();
411    }
412
413    private SoundResource getSoundResource(SpeechItem speechItem) {
414        SoundResource sr = null;
415        String text = speechItem.mText;
416        if (speechItem.mType == SpeechItem.SILENCE) {
417            // Do nothing if this is just silence
418        } else if (speechItem.mType == SpeechItem.EARCON) {
419            sr = mEarcons.get(text);
420        } else {
421            sr = mUtterances.get(text);
422        }
423        return sr;
424    }
425
426    private void dispatchSpeechCompletedCallbacks(String mark) {
427        Log.i("TTS callback", "dispatch started");
428        // Broadcast to all clients the new value.
429        final int N = mCallbacks.beginBroadcast();
430        for (int i = 0; i < N; i++) {
431            try {
432                mCallbacks.getBroadcastItem(i).markReached(mark);
433            } catch (RemoteException e) {
434                // The RemoteCallbackList will take care of removing
435                // the dead object for us.
436            }
437        }
438        mCallbacks.finishBroadcast();
439        Log.i("TTS callback", "dispatch completed to " + N);
440    }
441
442    private void processSpeechQueue() {
443        boolean speechQueueAvailable = false;
444        try {
445            speechQueueAvailable = speechQueueLock.tryLock();
446            if (!speechQueueAvailable) {
447                return;
448            }
449            if (mSpeechQueue.size() < 1) {
450                mIsSpeaking = false;
451                // Dispatch a completion here as this is the
452                // only place where speech completes normally.
453                // Nothing left to say in the queue is a special case
454                // that is always a "mark" - associated text is null.
455                dispatchSpeechCompletedCallbacks("");
456                return;
457            }
458
459            SpeechItem currentSpeechItem = mSpeechQueue.get(0);
460            mIsSpeaking = true;
461            SoundResource sr = getSoundResource(currentSpeechItem);
462            // Synth speech as needed - synthesizer should call
463            // processSpeechQueue to continue running the queue
464            Log.i("TTS processing: ", currentSpeechItem.mText);
465            if (sr == null) {
466                if (currentSpeechItem.mType == SpeechItem.SPEECH) {
467                    // TODO: Split text up into smaller chunks before accepting
468                    // them for processing.
469                    speakInternalOnly(currentSpeechItem.mText,
470                            currentSpeechItem.mParams);
471                } else {
472                    // This is either silence or an earcon that was missing
473                    silence(currentSpeechItem.mDuration);
474                }
475            } else {
476                cleanUpPlayer();
477                if (sr.mSourcePackageName == PKGNAME) {
478                    // Utterance is part of the TTS library
479                    mPlayer = MediaPlayer.create(this, sr.mResId);
480                } else if (sr.mSourcePackageName != null) {
481                    // Utterance is part of the app calling the library
482                    Context ctx;
483                    try {
484                        ctx = this.createPackageContext(sr.mSourcePackageName,
485                                0);
486                    } catch (NameNotFoundException e) {
487                        e.printStackTrace();
488                        mSpeechQueue.remove(0); // Remove it from the queue and
489                        // move on
490                        mIsSpeaking = false;
491                        return;
492                    }
493                    mPlayer = MediaPlayer.create(ctx, sr.mResId);
494                } else {
495                    // Utterance is coming from a file
496                    mPlayer = MediaPlayer.create(this, Uri.parse(sr.mFilename));
497                }
498
499                // Check if Media Server is dead; if it is, clear the queue and
500                // give up for now - hopefully, it will recover itself.
501                if (mPlayer == null) {
502                    mSpeechQueue.clear();
503                    mIsSpeaking = false;
504                    return;
505                }
506                mPlayer.setOnCompletionListener(this);
507                try {
508                    mPlayer.start();
509                } catch (IllegalStateException e) {
510                    mSpeechQueue.clear();
511                    mIsSpeaking = false;
512                    cleanUpPlayer();
513                    return;
514                }
515            }
516            if (mSpeechQueue.size() > 0) {
517                mSpeechQueue.remove(0);
518            }
519        } finally {
520            // This check is needed because finally will always run; even if the
521            // method returns somewhere in the try block.
522            if (speechQueueAvailable) {
523                speechQueueLock.unlock();
524            }
525        }
526    }
527
528    private void cleanUpPlayer() {
529        if (mPlayer != null) {
530            mPlayer.release();
531            mPlayer = null;
532        }
533    }
534
535    /**
536     * Synthesizes the given text using the specified queuing mode and
537     * parameters.
538     *
539     * @param text
540     *            The String of text that should be synthesized
541     * @param params
542     *            An ArrayList of parameters. The first element of this array
543     *            controls the type of voice to use.
544     * @param filename
545     *            The string that gives the full output filename; it should be
546     *            something like "/sdcard/myappsounds/mysound.wav".
547     * @return A boolean that indicates if the synthesis succeeded
548     */
549    private boolean synthesizeToFile(String text, ArrayList<String> params,
550            String filename, boolean calledFromApi) {
551        // Only stop everything if this is a call made by an outside app trying
552        // to
553        // use the API. Do NOT stop if this is a call from within the service as
554        // clearing the speech queue here would be a mistake.
555        if (calledFromApi) {
556            stop();
557        }
558        Log.i("TTS", "Synthesizing to " + filename);
559        boolean synthAvailable = false;
560        try {
561            synthAvailable = synthesizerLock.tryLock();
562            if (!synthAvailable) {
563                return false;
564            }
565            // Don't allow a filename that is too long
566            // TODO use platform constant
567            if (filename.length() > 250) {
568                return false;
569            }
570            nativeSynth.synthesizeToFile(text, filename);
571        } finally {
572            // This check is needed because finally will always run; even if the
573            // method returns somewhere in the try block.
574            if (synthAvailable) {
575                synthesizerLock.unlock();
576            }
577        }
578        Log.i("TTS", "Completed synthesis for " + filename);
579        return true;
580    }
581
582    @Override
583    public IBinder onBind(Intent intent) {
584        if (ACTION.equals(intent.getAction())) {
585            for (String category : intent.getCategories()) {
586                if (category.equals(CATEGORY)) {
587                    return mBinder;
588                }
589            }
590        }
591        return null;
592    }
593
594    private final android.speech.tts.ITts.Stub mBinder = new Stub() {
595
596        public void registerCallback(ITtsCallback cb) {
597            if (cb != null)
598                mCallbacks.register(cb);
599        }
600
601        public void unregisterCallback(ITtsCallback cb) {
602            if (cb != null)
603                mCallbacks.unregister(cb);
604        }
605
606        /**
607         * Speaks the given text using the specified queueing mode and
608         * parameters.
609         *
610         * @param text
611         *            The text that should be spoken
612         * @param queueMode
613         *            0 for no queue (interrupts all previous utterances), 1 for
614         *            queued
615         * @param params
616         *            An ArrayList of parameters. The first element of this
617         *            array controls the type of voice to use.
618         */
619        public void speak(String text, int queueMode, String[] params) {
620            ArrayList<String> speakingParams = new ArrayList<String>();
621            if (params != null) {
622                speakingParams = new ArrayList<String>(Arrays.asList(params));
623            }
624            mSelf.speak(text, queueMode, speakingParams);
625        }
626
627        /**
628         * Plays the earcon using the specified queueing mode and parameters.
629         *
630         * @param earcon
631         *            The earcon that should be played
632         * @param queueMode
633         *            0 for no queue (interrupts all previous utterances), 1 for
634         *            queued
635         * @param params
636         *            An ArrayList of parameters.
637         */
638        public void playEarcon(String earcon, int queueMode, String[] params) {
639            ArrayList<String> speakingParams = new ArrayList<String>();
640            if (params != null) {
641                speakingParams = new ArrayList<String>(Arrays.asList(params));
642            }
643            mSelf.playEarcon(earcon, queueMode, speakingParams);
644        }
645
646        /**
647         * Plays the silence using the specified queueing mode and parameters.
648         *
649         * @param duration
650         *            The duration of the silence that should be played
651         * @param queueMode
652         *            0 for no queue (interrupts all previous utterances), 1 for
653         *            queued
654         * @param params
655         *            An ArrayList of parameters.
656         */
657        public void playSilence(long duration, int queueMode, String[] params) {
658            ArrayList<String> speakingParams = new ArrayList<String>();
659            if (params != null) {
660                speakingParams = new ArrayList<String>(Arrays.asList(params));
661            }
662            mSelf.playSilence(duration, queueMode, speakingParams);
663        }
664
665        /**
666         * Stops all speech output and removes any utterances still in the
667         * queue.
668         */
669        public void stop() {
670            mSelf.stop();
671        }
672
673        /**
674         * Returns whether or not the TTS is speaking.
675         *
676         * @return Boolean to indicate whether or not the TTS is speaking
677         */
678        public boolean isSpeaking() {
679            return (mSelf.mIsSpeaking && (mSpeechQueue.size() < 1));
680        }
681
682        /**
683         * Adds a sound resource to the TTS.
684         *
685         * @param text
686         *            The text that should be associated with the sound resource
687         * @param packageName
688         *            The name of the package which has the sound resource
689         * @param resId
690         *            The resource ID of the sound within its package
691         */
692        public void addSpeech(String text, String packageName, int resId) {
693            mSelf.addSpeech(text, packageName, resId);
694        }
695
696        /**
697         * Adds a sound resource to the TTS.
698         *
699         * @param text
700         *            The text that should be associated with the sound resource
701         * @param filename
702         *            The filename of the sound resource. This must be a
703         *            complete path like: (/sdcard/mysounds/mysoundbite.mp3).
704         */
705        public void addSpeechFile(String text, String filename) {
706            mSelf.addSpeech(text, filename);
707        }
708
709        /**
710         * Adds a sound resource to the TTS as an earcon.
711         *
712         * @param earcon
713         *            The text that should be associated with the sound resource
714         * @param packageName
715         *            The name of the package which has the sound resource
716         * @param resId
717         *            The resource ID of the sound within its package
718         */
719        public void addEarcon(String earcon, String packageName, int resId) {
720            mSelf.addEarcon(earcon, packageName, resId);
721        }
722
723        /**
724         * Adds a sound resource to the TTS as an earcon.
725         *
726         * @param earcon
727         *            The text that should be associated with the sound resource
728         * @param filename
729         *            The filename of the sound resource. This must be a
730         *            complete path like: (/sdcard/mysounds/mysoundbite.mp3).
731         */
732        public void addEarconFile(String earcon, String filename) {
733            mSelf.addEarcon(earcon, filename);
734        }
735
736        /**
737         * Sets the speech rate for the TTS. Note that this will only have an
738         * effect on synthesized speech; it will not affect pre-recorded speech.
739         *
740         * @param speechRate
741         *            The speech rate that should be used
742         */
743        public void setSpeechRate(int speechRate) {
744            mSelf.setSpeechRate(speechRate);
745        }
746
747        /**
748         * Sets the pitch for the TTS. Note that this will only have an
749         * effect on synthesized speech; it will not affect pre-recorded speech.
750         *
751         * @param pitch
752         *            The pitch that should be used for the synthesized voice
753         */
754        public void setPitch(int pitch) {
755            mSelf.setPitch(pitch);
756        }
757
758        /**
759         * Sets the speech rate for the TTS, which affects the synthesized voice.
760         *
761         * @param lang  the three letter ISO language code.
762         * @param country  the three letter ISO country code.
763         * @param variant  the variant code associated with the country and language pair.
764         */
765        public void setLanguage(String lang, String country, String variant) {
766            mSelf.setLanguage(lang, country, variant);
767        }
768
769        /**
770         * Speaks the given text using the specified queueing mode and
771         * parameters.
772         *
773         * @param text
774         *            The String of text that should be synthesized
775         * @param params
776         *            An ArrayList of parameters. The first element of this
777         *            array controls the type of voice to use.
778         * @param filename
779         *            The string that gives the full output filename; it should
780         *            be something like "/sdcard/myappsounds/mysound.wav".
781         * @return A boolean that indicates if the synthesis succeeded
782         */
783        public boolean synthesizeToFile(String text, String[] params,
784                String filename) {
785            ArrayList<String> speakingParams = new ArrayList<String>();
786            if (params != null) {
787                speakingParams = new ArrayList<String>(Arrays.asList(params));
788            }
789            return mSelf.synthesizeToFile(text, speakingParams, filename, true);
790        }
791    };
792
793}
794