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